forked from recloudstream/cloudstream
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)) {
|
if (str.contains(appString)) {
|
||||||
for (api in OAuth2Apis) {
|
for (api in OAuth2Apis) {
|
||||||
if (str.contains("/${api.redirectUrl}")) {
|
if (str.contains("/${api.redirectUrl}")) {
|
||||||
api.handleRedirect(str)
|
try {
|
||||||
|
api.handleRedirect(str)
|
||||||
|
} catch (e : Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -10,7 +10,7 @@ interface OAuth2API {
|
||||||
val redirectUrl: String
|
val redirectUrl: String
|
||||||
|
|
||||||
// don't change this as all keys depend on it
|
// don't change this as all keys depend on it
|
||||||
val idPrefix : String
|
val idPrefix: String
|
||||||
|
|
||||||
fun handleRedirect(url: String)
|
fun handleRedirect(url: String)
|
||||||
fun authenticate()
|
fun authenticate()
|
||||||
|
@ -43,8 +43,8 @@ interface OAuth2API {
|
||||||
|
|
||||||
// used for active syncing
|
// used for active syncing
|
||||||
val SyncApis
|
val SyncApis
|
||||||
get() = listOf<SyncAPI>(
|
get() = listOf(
|
||||||
malApi, aniListApi
|
SyncRepo(malApi), SyncRepo(aniListApi)
|
||||||
)
|
)
|
||||||
|
|
||||||
const val appString = "cloudstreamapp"
|
const val appString = "cloudstreamapp"
|
||||||
|
|
|
@ -3,6 +3,26 @@ package com.lagradost.cloudstream3.syncproviders
|
||||||
import com.lagradost.cloudstream3.ShowStatus
|
import com.lagradost.cloudstream3.ShowStatus
|
||||||
|
|
||||||
interface SyncAPI : OAuth2API {
|
interface SyncAPI : OAuth2API {
|
||||||
|
val icon: Int
|
||||||
|
val mainUrl: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
-1 -> None
|
||||||
|
0 -> Watching
|
||||||
|
1 -> Completed
|
||||||
|
2 -> OnHold
|
||||||
|
3 -> Dropped
|
||||||
|
4 -> PlanToWatch
|
||||||
|
5 -> ReWatching
|
||||||
|
*/
|
||||||
|
suspend fun score(id: String, status: SyncStatus): Boolean
|
||||||
|
|
||||||
|
suspend fun getStatus(id: String): SyncStatus?
|
||||||
|
|
||||||
|
suspend fun getResult(id: String): SyncResult?
|
||||||
|
|
||||||
|
suspend fun search(name: String): List<SyncSearchResult>?
|
||||||
|
|
||||||
data class SyncSearchResult(
|
data class SyncSearchResult(
|
||||||
val name: String,
|
val name: String,
|
||||||
val syncApiName: String,
|
val syncApiName: String,
|
||||||
|
@ -48,7 +68,7 @@ interface SyncAPI : OAuth2API {
|
||||||
var synopsis: String? = null,
|
var synopsis: String? = null,
|
||||||
var airStatus: ShowStatus? = null,
|
var airStatus: ShowStatus? = null,
|
||||||
var nextAiring: SyncNextAiring? = null,
|
var nextAiring: SyncNextAiring? = null,
|
||||||
var studio: String? = null,
|
var studio: List<String>? = null,
|
||||||
var genres: List<String>? = null,
|
var genres: List<String>? = null,
|
||||||
var trailerUrl: String? = null,
|
var trailerUrl: String? = null,
|
||||||
|
|
||||||
|
@ -62,24 +82,4 @@ interface SyncAPI : OAuth2API {
|
||||||
var actors: List<SyncActor>? = null,
|
var actors: List<SyncActor>? = null,
|
||||||
var characters: List<SyncCharacter>? = null,
|
var characters: List<SyncCharacter>? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
val icon: Int
|
|
||||||
|
|
||||||
val mainUrl: String
|
|
||||||
suspend fun search(name: String): List<SyncSearchResult>?
|
|
||||||
|
|
||||||
/**
|
|
||||||
-1 -> None
|
|
||||||
0 -> Watching
|
|
||||||
1 -> Completed
|
|
||||||
2 -> OnHold
|
|
||||||
3 -> Dropped
|
|
||||||
4 -> PlanToWatch
|
|
||||||
5 -> ReWatching
|
|
||||||
*/
|
|
||||||
suspend fun score(id: String, status: SyncStatus): Boolean
|
|
||||||
|
|
||||||
suspend fun getStatus(id: String): SyncStatus?
|
|
||||||
|
|
||||||
suspend fun getResult(id: String): SyncResult?
|
|
||||||
}
|
}
|
|
@ -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) {
|
override fun handleRedirect(url: String) {
|
||||||
try {
|
val sanitizer =
|
||||||
val sanitizer =
|
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
val token = sanitizer["access_token"]!!
|
||||||
val token = sanitizer["access_token"]!!
|
val expiresIn = sanitizer["expires_in"]!!
|
||||||
val expiresIn = sanitizer["expires_in"]!!
|
|
||||||
|
|
||||||
val endTime = unixTime + expiresIn.toLong()
|
val endTime = unixTime + expiresIn.toLong()
|
||||||
|
|
||||||
switchToNewAccount()
|
switchToNewAccount()
|
||||||
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
|
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
|
||||||
setKey(accountId, ANILIST_TOKEN_KEY, token)
|
setKey(accountId, ANILIST_TOKEN_KEY, token)
|
||||||
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
|
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
|
||||||
ioSafe {
|
ioSafe {
|
||||||
getUser()
|
getUser()
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +334,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initGetUser() {
|
fun initGetUser() {
|
||||||
if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return
|
if (getAuth() == null) return
|
||||||
ioSafe {
|
ioSafe {
|
||||||
getUser()
|
getUser()
|
||||||
}
|
}
|
||||||
|
@ -351,7 +347,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)!!
|
)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDataAboutId(id: Int): AniListTitleHolder? {
|
private suspend fun getDataAboutId(id: Int): AniListTitleHolder? {
|
||||||
val q =
|
val q =
|
||||||
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
||||||
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
||||||
|
@ -369,71 +365,56 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
try {
|
|
||||||
val data = postApi("https://graphql.anilist.co", q, true)
|
|
||||||
var d: GetDataRoot? = null
|
|
||||||
try {
|
|
||||||
d = mapper.readValue<GetDataRoot>(data)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
println("AniList json failed")
|
|
||||||
}
|
|
||||||
if (d == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val main = d.data.Media
|
val data = postApi(q, true)
|
||||||
if (main.mediaListEntry != null) {
|
val d = mapper.readValue<GetDataRoot>(data ?: return null)
|
||||||
return AniListTitleHolder(
|
|
||||||
title = main.title,
|
val main = d.data.Media
|
||||||
id = id,
|
if (main.mediaListEntry != null) {
|
||||||
isFavourite = main.isFavourite,
|
return AniListTitleHolder(
|
||||||
progress = main.mediaListEntry.progress,
|
title = main.title,
|
||||||
episodes = main.episodes,
|
id = id,
|
||||||
score = main.mediaListEntry.score,
|
isFavourite = main.isFavourite,
|
||||||
type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)),
|
progress = main.mediaListEntry.progress,
|
||||||
)
|
episodes = main.episodes,
|
||||||
} else {
|
score = main.mediaListEntry.score,
|
||||||
return AniListTitleHolder(
|
type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)),
|
||||||
title = main.title,
|
)
|
||||||
id = id,
|
} else {
|
||||||
isFavourite = main.isFavourite,
|
return AniListTitleHolder(
|
||||||
progress = 0,
|
title = main.title,
|
||||||
episodes = main.episodes,
|
id = id,
|
||||||
score = 0,
|
isFavourite = main.isFavourite,
|
||||||
type = AniListStatusType.None,
|
progress = 0,
|
||||||
)
|
episodes = main.episodes,
|
||||||
}
|
score = 0,
|
||||||
} catch (e: Exception) {
|
type = AniListStatusType.None,
|
||||||
logError(e)
|
)
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun postApi(url: String, q: String, cache: Boolean = false): String {
|
private fun getAuth(): String? {
|
||||||
return try {
|
return getKey(
|
||||||
if (!checkToken()) {
|
accountId,
|
||||||
// println("VARS_ " + vars)
|
ANILIST_TOKEN_KEY
|
||||||
app.post(
|
)
|
||||||
"https://graphql.anilist.co/",
|
}
|
||||||
headers = mapOf(
|
|
||||||
"Authorization" to "Bearer " + getKey(
|
private suspend fun postApi(q: String, cache: Boolean = false): String? {
|
||||||
accountId,
|
return if (!checkToken()) {
|
||||||
ANILIST_TOKEN_KEY,
|
app.post(
|
||||||
""
|
"https://graphql.anilist.co/",
|
||||||
)!!,
|
headers = mapOf(
|
||||||
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
"Authorization" to "Bearer " + (getAuth() ?: return null),
|
||||||
),
|
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
||||||
cacheTime = 0,
|
),
|
||||||
data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
|
cacheTime = 0,
|
||||||
timeout = 5 // REASONABLE TIMEOUT
|
data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
|
||||||
).text.replace("\\/", "/")
|
timeout = 5 // REASONABLE TIMEOUT
|
||||||
} else {
|
).text.replace("\\/", "/")
|
||||||
""
|
} else {
|
||||||
}
|
null
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -515,12 +496,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
|
suspend fun getAnilistAnimeListSmart(): Array<Lists>? {
|
||||||
if (getKey<String>(
|
if (getAuth() == null) return null
|
||||||
accountId,
|
|
||||||
ANILIST_TOKEN_KEY,
|
|
||||||
null
|
|
||||||
) == null
|
|
||||||
) return null
|
|
||||||
|
|
||||||
if (checkToken()) return null
|
if (checkToken()) return null
|
||||||
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
|
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
|
||||||
|
@ -536,19 +512,18 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFullAnilistList(): FullAnilistList? {
|
private suspend fun getFullAnilistList(): FullAnilistList? {
|
||||||
try {
|
var userID: Int? = null
|
||||||
var userID: Int? = null
|
/** WARNING ASSUMES ONE USER! **/
|
||||||
/** WARNING ASSUMES ONE USER! **/
|
getKeys(ANILIST_USER_KEY)?.forEach { key ->
|
||||||
getKeys(ANILIST_USER_KEY)?.forEach { key ->
|
getKey<AniListUser>(key, null)?.let {
|
||||||
getKey<AniListUser>(key, null)?.let {
|
userID = it.id
|
||||||
userID = it.id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val fixedUserID = userID ?: return null
|
val fixedUserID = userID ?: return null
|
||||||
val mediaType = "ANIME"
|
val mediaType = "ANIME"
|
||||||
|
|
||||||
val query = """
|
val query = """
|
||||||
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
|
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
|
||||||
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
|
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
|
||||||
lists {
|
lists {
|
||||||
|
@ -588,13 +563,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
val text = postApi("https://graphql.anilist.co", query)
|
val text = postApi(query)
|
||||||
return text.toKotlinObject()
|
return text?.toKotlinObject()
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun toggleLike(id: Int): Boolean {
|
suspend fun toggleLike(id: Int): Boolean {
|
||||||
|
@ -610,7 +580,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
val data = postApi("https://graphql.anilist.co", q)
|
val data = postApi(q)
|
||||||
return data != ""
|
return data != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -620,14 +590,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
score: Int?,
|
score: Int?,
|
||||||
progress: Int?
|
progress: Int?
|
||||||
): Boolean {
|
): Boolean {
|
||||||
try {
|
val q =
|
||||||
val q =
|
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
|
||||||
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
|
aniListStatusString[maxOf(
|
||||||
aniListStatusString[maxOf(
|
0,
|
||||||
0,
|
type.value
|
||||||
type.value
|
)]
|
||||||
)]
|
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
|
||||||
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
|
|
||||||
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
|
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
|
||||||
id
|
id
|
||||||
status
|
status
|
||||||
|
@ -635,12 +604,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
score
|
score
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
val data = postApi("https://graphql.anilist.co", q)
|
val data = postApi(q)
|
||||||
return data != ""
|
return data != ""
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getUser(setSettings: Boolean = true): AniListUser? {
|
private suspend fun getUser(setSettings: Boolean = true): AniListUser? {
|
||||||
|
@ -661,29 +626,24 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
try {
|
val data = postApi(q)
|
||||||
val data = postApi("https://graphql.anilist.co", q)
|
if (data == "") return null
|
||||||
if (data == "") return null
|
val userData = mapper.readValue<AniListRoot>(data ?: return null)
|
||||||
val userData = mapper.readValue<AniListRoot>(data)
|
val u = userData.data.Viewer
|
||||||
val u = userData.data.Viewer
|
val user = AniListUser(
|
||||||
val user = AniListUser(
|
u.id,
|
||||||
u.id,
|
u.name,
|
||||||
u.name,
|
u.avatar.large,
|
||||||
u.avatar.large,
|
)
|
||||||
)
|
if (setSettings) {
|
||||||
if (setSettings) {
|
setKey(accountId, ANILIST_USER_KEY, user)
|
||||||
setKey(accountId, ANILIST_USER_KEY, user)
|
registerAccount()
|
||||||
registerAccount()
|
|
||||||
}
|
|
||||||
/* // TODO FIX FAVS
|
|
||||||
for(i in u.favourites.anime.nodes) {
|
|
||||||
println("FFAV:" + i.id)
|
|
||||||
}*/
|
|
||||||
return user
|
|
||||||
} catch (e: java.lang.Exception) {
|
|
||||||
logError(e)
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
/* // TODO FIX FAVS
|
||||||
|
for(i in u.favourites.anime.nodes) {
|
||||||
|
println("FFAV:" + i.id)
|
||||||
|
}*/
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAllSeasons(id: Int): List<SeasonResponse?> {
|
suspend fun getAllSeasons(id: Int): List<SeasonResponse?> {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.ShowStatus
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
|
@ -45,20 +46,28 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override fun loginInfo(): OAuth2API.LoginInfo? {
|
override fun loginInfo(): OAuth2API.LoginInfo? {
|
||||||
//getMalUser(true)?
|
//getMalUser(true)?
|
||||||
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
||||||
return OAuth2API.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex)
|
return OAuth2API.LoginInfo(
|
||||||
|
profilePicture = user.picture,
|
||||||
|
name = user.name,
|
||||||
|
accountIndex = accountIndex
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
|
private fun getAuth(): String? {
|
||||||
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
return getKey(
|
||||||
val auth = getKey<String>(
|
|
||||||
accountId,
|
accountId,
|
||||||
MAL_TOKEN_KEY
|
MAL_TOKEN_KEY
|
||||||
) ?: return emptyList()
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult> {
|
||||||
|
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
|
||||||
|
val auth = getAuth() ?: return emptyList()
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
url, headers = mapOf(
|
url, headers = mapOf(
|
||||||
"Authorization" to "Bearer " + auth,
|
"Authorization" to "Bearer $auth",
|
||||||
), cacheTime = 0
|
), cacheTime = 0
|
||||||
).text
|
).text
|
||||||
return mapper.readValue<MalSearch>(res).data.map {
|
return mapper.readValue<MalSearch>(res).data.map {
|
||||||
|
@ -73,7 +82,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun score(id: String, status : SyncAPI.SyncStatus): Boolean {
|
override suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
|
||||||
return setScoreRequest(
|
return setScoreRequest(
|
||||||
id.toIntOrNull() ?: return false,
|
id.toIntOrNull() ?: return false,
|
||||||
fromIntToAnimeStatus(status.status),
|
fromIntToAnimeStatus(status.status),
|
||||||
|
@ -82,25 +91,156 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class MalAnime(
|
||||||
|
@JsonProperty("id") val id: Int?,
|
||||||
|
@JsonProperty("title") val title: String?,
|
||||||
|
@JsonProperty("main_picture") val mainPicture: MainPicture?,
|
||||||
|
@JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
|
||||||
|
@JsonProperty("start_date") val startDate: String?,
|
||||||
|
@JsonProperty("end_date") val endDate: String?,
|
||||||
|
@JsonProperty("synopsis") val synopsis: String?,
|
||||||
|
@JsonProperty("mean") val mean: Double?,
|
||||||
|
@JsonProperty("rank") val rank: Int?,
|
||||||
|
@JsonProperty("popularity") val popularity: Int?,
|
||||||
|
@JsonProperty("num_list_users") val numListUsers: Int?,
|
||||||
|
@JsonProperty("num_scoring_users") val numScoringUsers: Int?,
|
||||||
|
@JsonProperty("nsfw") val nsfw: String?,
|
||||||
|
@JsonProperty("created_at") val createdAt: String?,
|
||||||
|
@JsonProperty("updated_at") val updatedAt: String?,
|
||||||
|
@JsonProperty("media_type") val mediaType: String?,
|
||||||
|
@JsonProperty("status") val status: String?,
|
||||||
|
@JsonProperty("genres") val genres: ArrayList<Genres>,
|
||||||
|
@JsonProperty("my_list_status") val myListStatus: MyListStatus?,
|
||||||
|
@JsonProperty("num_episodes") val numEpisodes: Int?,
|
||||||
|
@JsonProperty("start_season") val startSeason: StartSeason?,
|
||||||
|
@JsonProperty("broadcast") val broadcast: Broadcast?,
|
||||||
|
@JsonProperty("source") val source: String?,
|
||||||
|
@JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
|
||||||
|
@JsonProperty("rating") val rating: String?,
|
||||||
|
@JsonProperty("pictures") val pictures: ArrayList<MainPicture>,
|
||||||
|
@JsonProperty("background") val background: String?,
|
||||||
|
@JsonProperty("related_anime") val relatedAnime: ArrayList<RelatedAnime>,
|
||||||
|
@JsonProperty("related_manga") val relatedManga: ArrayList<String>,
|
||||||
|
@JsonProperty("recommendations") val recommendations: ArrayList<Recommendations>,
|
||||||
|
@JsonProperty("studios") val studios: ArrayList<Studios>,
|
||||||
|
@JsonProperty("statistics") val statistics: Statistics?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Recommendations(
|
||||||
|
@JsonProperty("node") val node: Node? = null,
|
||||||
|
@JsonProperty("num_recommendations") val numRecommendations: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Studios(
|
||||||
|
@JsonProperty("id") val id: Int? = null,
|
||||||
|
@JsonProperty("name") val name: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
data class MyListStatus(
|
||||||
|
@JsonProperty("status") val status: String? = null,
|
||||||
|
@JsonProperty("score") val score: Int? = null,
|
||||||
|
@JsonProperty("num_episodes_watched") val numEpisodesWatched: Int? = null,
|
||||||
|
@JsonProperty("is_rewatching") val isRewatching: Boolean? = null,
|
||||||
|
@JsonProperty("updated_at") val updatedAt: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RelatedAnime(
|
||||||
|
@JsonProperty("node") val node: Node? = null,
|
||||||
|
@JsonProperty("relation_type") val relationType: String? = null,
|
||||||
|
@JsonProperty("relation_type_formatted") val relationTypeFormatted: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Status(
|
||||||
|
@JsonProperty("watching") val watching: String? = null,
|
||||||
|
@JsonProperty("completed") val completed: String? = null,
|
||||||
|
@JsonProperty("on_hold") val onHold: String? = null,
|
||||||
|
@JsonProperty("dropped") val dropped: String? = null,
|
||||||
|
@JsonProperty("plan_to_watch") val planToWatch: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Statistics(
|
||||||
|
@JsonProperty("status") val status: Status? = null,
|
||||||
|
@JsonProperty("num_list_users") val numListUsers: Int? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun parseDate(string: String?): Long? {
|
||||||
|
return try {
|
||||||
|
SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toSearchResult(node : Node?) : SyncAPI.SyncSearchResult? {
|
||||||
|
return SyncAPI.SyncSearchResult(
|
||||||
|
name = node?.title ?: return null,
|
||||||
|
syncApiName = this.name,
|
||||||
|
id = node.id.toString(),
|
||||||
|
url = "https://myanimelist.net/anime/${node.id}",
|
||||||
|
posterUrl = node.main_picture?.large
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
TODO("Not yet implemented")
|
val url =
|
||||||
|
"https://api.myanimelist.net/v2/anime/$internalId?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,created_at,updated_at,media_type,status,genres,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,related_manga,recommendations,studios,statistics"
|
||||||
|
val res = app.get(
|
||||||
|
url, headers = mapOf(
|
||||||
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
|
)
|
||||||
|
).text
|
||||||
|
return mapper.readValue<MalAnime>(res).let { malAnime ->
|
||||||
|
SyncAPI.SyncResult(
|
||||||
|
id = malAnime.id?.toString()!!,
|
||||||
|
totalEpisodes = malAnime.numEpisodes,
|
||||||
|
title = malAnime.title,
|
||||||
|
publicScore = malAnime.mean?.toFloat()?.times(100)?.toInt(),
|
||||||
|
duration = malAnime.averageEpisodeDuration,
|
||||||
|
synopsis = malAnime.synopsis,
|
||||||
|
airStatus = when (malAnime.status) {
|
||||||
|
"finished_airing" -> ShowStatus.Completed
|
||||||
|
"airing" -> ShowStatus.Ongoing
|
||||||
|
else -> null
|
||||||
|
},
|
||||||
|
nextAiring = null,
|
||||||
|
studio = malAnime.studios.mapNotNull { it.name },
|
||||||
|
genres = malAnime.genres.map { it.name },
|
||||||
|
trailerUrl = null,
|
||||||
|
startDate = parseDate(malAnime.startDate),
|
||||||
|
endDate = parseDate(malAnime.endDate),
|
||||||
|
recommendations = malAnime.recommendations.mapNotNull { rec ->
|
||||||
|
val node = rec.node ?: return@mapNotNull null
|
||||||
|
toSearchResult(node)
|
||||||
|
},
|
||||||
|
nextSeason = malAnime.relatedAnime.firstOrNull {
|
||||||
|
return@firstOrNull it.relationType == "sequel"
|
||||||
|
}?.let { toSearchResult(it.node) },
|
||||||
|
prevSeason = malAnime.relatedAnime.firstOrNull {
|
||||||
|
return@firstOrNull it.relationType == "prequel"
|
||||||
|
}?.let { toSearchResult(it.node) },
|
||||||
|
actors = null,
|
||||||
|
characters = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
|
||||||
val internalId = id.toIntOrNull() ?: return null
|
val internalId = id.toIntOrNull() ?: return null
|
||||||
|
|
||||||
val data = getDataAboutMalId(internalId)?.my_list_status ?: return null
|
val data = getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
|
||||||
return SyncAPI.SyncStatus(
|
return SyncAPI.SyncStatus(
|
||||||
score = data.score,
|
score = data?.score,
|
||||||
status = malStatusAsString.indexOf(data.status),
|
status = malStatusAsString.indexOf(data?.status),
|
||||||
isFavorite = null,
|
isFavorite = null,
|
||||||
watchedEpisodes = data.num_episodes_watched,
|
watchedEpisodes = data?.num_episodes_watched,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val malStatusAsString = arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch")
|
private val malStatusAsString =
|
||||||
|
arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch")
|
||||||
|
|
||||||
const val MAL_USER_KEY: String = "mal_user" // user data like profile
|
const val MAL_USER_KEY: String = "mal_user" // user data like profile
|
||||||
const val MAL_CACHED_LIST: String = "mal_cached_list"
|
const val MAL_CACHED_LIST: String = "mal_cached_list"
|
||||||
|
@ -111,39 +251,35 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleRedirect(url: String) {
|
override fun handleRedirect(url: String) {
|
||||||
try {
|
val sanitizer =
|
||||||
val sanitizer =
|
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
val state = sanitizer["state"]!!
|
||||||
val state = sanitizer["state"]!!
|
if (state == "RequestID$requestId") {
|
||||||
if (state == "RequestID$requestId") {
|
val currentCode = sanitizer["code"]!!
|
||||||
val currentCode = sanitizer["code"]!!
|
ioSafe {
|
||||||
ioSafe {
|
var res = ""
|
||||||
var res = ""
|
try {
|
||||||
try {
|
//println("cc::::: " + codeVerifier)
|
||||||
//println("cc::::: " + codeVerifier)
|
res = app.post(
|
||||||
res = app.post(
|
"https://myanimelist.net/v1/oauth2/token",
|
||||||
"https://myanimelist.net/v1/oauth2/token",
|
data = mapOf(
|
||||||
data = mapOf(
|
"client_id" to key,
|
||||||
"client_id" to key,
|
"code" to currentCode,
|
||||||
"code" to currentCode,
|
"code_verifier" to codeVerifier,
|
||||||
"code_verifier" to codeVerifier,
|
"grant_type" to "authorization_code"
|
||||||
"grant_type" to "authorization_code"
|
)
|
||||||
)
|
).text
|
||||||
).text
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
e.printStackTrace()
|
||||||
e.printStackTrace()
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (res != "") {
|
if (res != "") {
|
||||||
switchToNewAccount()
|
switchToNewAccount()
|
||||||
storeToken(res)
|
storeToken(res)
|
||||||
getMalUser()
|
getMalUser()
|
||||||
setKey(MAL_SHOULD_UPDATE_LIST, true)
|
setKey(MAL_SHOULD_UPDATE_LIST, true)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,44 +413,35 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
@JsonProperty("start_time") val start_time: String?
|
@JsonProperty("start_time") val start_time: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getMalAnimeListCached(): Array<Data>? {
|
private fun getMalAnimeListCached(): Array<Data>? {
|
||||||
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMalAnimeListSmart(): Array<Data>? {
|
suspend fun getMalAnimeListSmart(): Array<Data>? {
|
||||||
if (getKey<String>(
|
if (getAuth() == null) return null
|
||||||
accountId,
|
|
||||||
MAL_TOKEN_KEY
|
|
||||||
) == null
|
|
||||||
) return null
|
|
||||||
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
|
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
|
||||||
val list = getMalAnimeList()
|
val list = getMalAnimeList()
|
||||||
if (list != null) {
|
setKey(MAL_CACHED_LIST, list)
|
||||||
setKey(MAL_CACHED_LIST, list)
|
setKey(MAL_SHOULD_UPDATE_LIST, false)
|
||||||
setKey(MAL_SHOULD_UPDATE_LIST, false)
|
|
||||||
}
|
|
||||||
list
|
list
|
||||||
} else {
|
} else {
|
||||||
getMalAnimeListCached()
|
getMalAnimeListCached()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getMalAnimeList(): Array<Data>? {
|
private suspend fun getMalAnimeList(): Array<Data> {
|
||||||
return try {
|
checkMalToken()
|
||||||
checkMalToken()
|
var offset = 0
|
||||||
var offset = 0
|
val fullList = mutableListOf<Data>()
|
||||||
val fullList = mutableListOf<Data>()
|
val offsetRegex = Regex("""offset=(\d+)""")
|
||||||
val offsetRegex = Regex("""offset=(\d+)""")
|
while (true) {
|
||||||
while (true) {
|
val data: MalList = getMalAnimeListSlice(offset) ?: break
|
||||||
val data: MalList = getMalAnimeListSlice(offset) ?: break
|
fullList.addAll(data.data)
|
||||||
fullList.addAll(data.data)
|
offset =
|
||||||
offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break
|
data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() }
|
||||||
}
|
?: break
|
||||||
fullList.toTypedArray()
|
|
||||||
//mapper.readValue<MalAnime>(res)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
return fullList.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertToStatus(string: String): MalStatusType {
|
fun convertToStatus(string: String): MalStatusType {
|
||||||
|
@ -323,43 +450,30 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
|
||||||
val user = "@me"
|
val user = "@me"
|
||||||
val auth = getKey<String>(
|
val auth = getAuth() ?: return null
|
||||||
accountId,
|
// Very lackluster docs
|
||||||
MAL_TOKEN_KEY
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
||||||
) ?: return null
|
val url =
|
||||||
return try {
|
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
|
||||||
// Very lackluster docs
|
val res = app.get(
|
||||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
url, headers = mapOf(
|
||||||
val url =
|
"Authorization" to "Bearer $auth",
|
||||||
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
|
), cacheTime = 0
|
||||||
val res = app.get(
|
).text
|
||||||
url, headers = mapOf(
|
return res.toKotlinObject()
|
||||||
"Authorization" to "Bearer $auth",
|
|
||||||
), cacheTime = 0
|
|
||||||
).text
|
|
||||||
res.toKotlinObject()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDataAboutMalId(id: Int): MalAnime? {
|
private suspend fun getDataAboutMalId(id: Int): SmallMalAnime? {
|
||||||
return try {
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
||||||
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
|
val url =
|
||||||
val url = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
|
"https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status"
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
url, headers = mapOf(
|
url, headers = mapOf(
|
||||||
"Authorization" to "Bearer " + getKey<String>(
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
accountId,
|
), cacheTime = 0
|
||||||
MAL_TOKEN_KEY
|
).text
|
||||||
)!!
|
|
||||||
), cacheTime = 0
|
return mapper.readValue<SmallMalAnime>(res)
|
||||||
).text
|
|
||||||
mapper.readValue<MalAnime>(res)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setAllMalData() {
|
suspend fun setAllMalData() {
|
||||||
|
@ -372,14 +486,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
val res = app.get(
|
val res = app.get(
|
||||||
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
|
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
|
||||||
headers = mapOf(
|
headers = mapOf(
|
||||||
"Authorization" to "Bearer " + getKey<String>(
|
"Authorization" to "Bearer " + (getAuth() ?: return)
|
||||||
accountId,
|
|
||||||
MAL_TOKEN_KEY
|
|
||||||
)!!
|
|
||||||
), cacheTime = 0
|
), cacheTime = 0
|
||||||
).text
|
).text
|
||||||
val values = mapper.readValue<MalRoot>(res)
|
val values = mapper.readValue<MalRoot>(res)
|
||||||
val titles = values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
|
val titles =
|
||||||
|
values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
|
||||||
for (t in titles) {
|
for (t in titles) {
|
||||||
allTitles[t.id] = t
|
allTitles[t.id] = t
|
||||||
}
|
}
|
||||||
|
@ -389,41 +501,37 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
||||||
|
// No time remaining if the show has already ended
|
||||||
try {
|
try {
|
||||||
// No time remaining if the show has already ended
|
endDate?.let {
|
||||||
try {
|
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
|
||||||
endDate?.let {
|
|
||||||
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
|
|
||||||
}
|
|
||||||
} catch (e: ParseException) {
|
|
||||||
logError(e)
|
|
||||||
}
|
}
|
||||||
|
} catch (e: ParseException) {
|
||||||
// Unparseable date: "2021 7 4 other null"
|
|
||||||
// Weekday: other, date: null
|
|
||||||
if (date.contains("null") || date.contains("other")) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentDate = Calendar.getInstance()
|
|
||||||
val currentMonth = currentDate.get(Calendar.MONTH) + 1
|
|
||||||
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
|
|
||||||
val currentYear = currentDate.get(Calendar.YEAR)
|
|
||||||
|
|
||||||
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
|
|
||||||
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
|
|
||||||
val parsedDate = dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
|
|
||||||
val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000
|
|
||||||
|
|
||||||
// if it has already aired this week add a week to the timer
|
|
||||||
val updatedTimeDiff =
|
|
||||||
if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff
|
|
||||||
return secondsToReadable(updatedTimeDiff.toInt(), "Now")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
|
// Unparseable date: "2021 7 4 other null"
|
||||||
|
// Weekday: other, date: null
|
||||||
|
if (date.contains("null") || date.contains("other")) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentDate = Calendar.getInstance()
|
||||||
|
val currentMonth = currentDate.get(Calendar.MONTH) + 1
|
||||||
|
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
|
||||||
|
val currentYear = currentDate.get(Calendar.YEAR)
|
||||||
|
|
||||||
|
val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
|
||||||
|
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
|
||||||
|
val parsedDate =
|
||||||
|
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
|
||||||
|
val timeDiff = (parsedDate.time - System.currentTimeMillis()) / 1000
|
||||||
|
|
||||||
|
// if it has already aired this week add a week to the timer
|
||||||
|
val updatedTimeDiff =
|
||||||
|
if (timeDiff > -60 * 60 * 24 * 7 && timeDiff < 0) timeDiff + 60 * 60 * 24 * 7 else timeDiff
|
||||||
|
return secondsToReadable(updatedTimeDiff.toInt(), "Now")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkMalToken() {
|
private suspend fun checkMalToken() {
|
||||||
|
@ -438,27 +546,19 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
|
||||||
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
private suspend fun getMalUser(setSettings: Boolean = true): MalUser? {
|
||||||
checkMalToken()
|
checkMalToken()
|
||||||
return try {
|
val res = app.get(
|
||||||
val res = app.get(
|
"https://api.myanimelist.net/v2/users/@me",
|
||||||
"https://api.myanimelist.net/v2/users/@me",
|
headers = mapOf(
|
||||||
headers = mapOf(
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
"Authorization" to "Bearer " + getKey<String>(
|
), cacheTime = 0
|
||||||
accountId,
|
).text
|
||||||
MAL_TOKEN_KEY
|
|
||||||
)!!
|
|
||||||
), cacheTime = 0
|
|
||||||
).text
|
|
||||||
|
|
||||||
val user = mapper.readValue<MalUser>(res)
|
val user = mapper.readValue<MalUser>(res)
|
||||||
if (setSettings) {
|
if (setSettings) {
|
||||||
setKey(accountId, MAL_USER_KEY, user)
|
setKey(accountId, MAL_USER_KEY, user)
|
||||||
registerAccount()
|
registerAccount()
|
||||||
}
|
|
||||||
user
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MalStatusType(var value: Int) {
|
enum class MalStatusType(var value: Int) {
|
||||||
|
@ -483,7 +583,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setScoreRequest(
|
private suspend fun setScoreRequest(
|
||||||
id: Int,
|
id: Int,
|
||||||
status: MalStatusType? = null,
|
status: MalStatusType? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
|
@ -495,22 +595,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
score,
|
score,
|
||||||
num_watched_episodes
|
num_watched_episodes
|
||||||
)
|
)
|
||||||
if (res != "") {
|
|
||||||
return try {
|
return if (res.isNullOrBlank()) {
|
||||||
val malStatus = mapper.readValue<MalStatus>(res)
|
false
|
||||||
if (allTitles.containsKey(id)) {
|
|
||||||
val currentTitle = allTitles[id]!!
|
|
||||||
allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name)
|
|
||||||
} else {
|
|
||||||
allTitles[id] = MalTitleHolder(malStatus, id, "")
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return false
|
val malStatus = mapper.readValue<MalStatus>(res)
|
||||||
|
if (allTitles.containsKey(id)) {
|
||||||
|
val currentTitle = allTitles[id]!!
|
||||||
|
allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name)
|
||||||
|
} else {
|
||||||
|
allTitles[id] = MalTitleHolder(malStatus, id, "")
|
||||||
|
}
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -519,26 +615,18 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
status: String? = null,
|
status: String? = null,
|
||||||
score: Int? = null,
|
score: Int? = null,
|
||||||
num_watched_episodes: Int? = null,
|
num_watched_episodes: Int? = null,
|
||||||
): String {
|
): String? {
|
||||||
return try {
|
return app.put(
|
||||||
app.put(
|
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
|
||||||
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
|
headers = mapOf(
|
||||||
headers = mapOf(
|
"Authorization" to "Bearer " + (getAuth() ?: return null)
|
||||||
"Authorization" to "Bearer " + getKey<String>(
|
),
|
||||||
accountId,
|
data = mapOf(
|
||||||
MAL_TOKEN_KEY
|
"status" to status,
|
||||||
)!!
|
"score" to score?.toString(),
|
||||||
),
|
"num_watched_episodes" to num_watched_episodes?.toString()
|
||||||
data = mapOf(
|
)
|
||||||
"status" to status,
|
).text
|
||||||
"score" to score?.toString(),
|
|
||||||
"num_watched_episodes" to num_watched_episodes?.toString()
|
|
||||||
)
|
|
||||||
).text
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -591,7 +679,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Used for getDataAboutId()
|
// Used for getDataAboutId()
|
||||||
data class MalAnime(
|
data class SmallMalAnime(
|
||||||
@JsonProperty("id") val id: Int,
|
@JsonProperty("id") val id: Int,
|
||||||
@JsonProperty("title") val title: String?,
|
@JsonProperty("title") val title: String?,
|
||||||
@JsonProperty("num_episodes") val num_episodes: Int,
|
@JsonProperty("num_episodes") val num_episodes: Int,
|
||||||
|
|
|
@ -12,8 +12,7 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
|
||||||
import com.lagradost.cloudstream3.HomePageList
|
import com.lagradost.cloudstream3.HomePageList
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
|
@ -21,8 +20,11 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
||||||
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||||
import com.lagradost.cloudstream3.ui.search.*
|
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
||||||
|
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||||
|
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
@ -31,11 +33,10 @@ import kotlinx.android.synthetic.main.fragment_search.*
|
||||||
import kotlinx.android.synthetic.main.quick_search.*
|
import kotlinx.android.synthetic.main.quick_search.*
|
||||||
import java.util.concurrent.locks.ReentrantLock
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
class QuickSearchFragment : Fragment() {
|
||||||
companion object {
|
companion object {
|
||||||
fun pushSearch(activity: Activity?, autoSearch: String? = null) {
|
fun pushSearch(activity: Activity?, autoSearch: String? = null) {
|
||||||
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
|
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
|
||||||
putBoolean("mainapi", true)
|
|
||||||
autoSearch?.let {
|
autoSearch?.let {
|
||||||
putString(
|
putString(
|
||||||
"autosearch",
|
"autosearch",
|
||||||
|
@ -49,18 +50,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pushSync(
|
|
||||||
activity: Activity?,
|
|
||||||
autoSearch: String? = null,
|
|
||||||
callback: (SearchClickCallback) -> Unit
|
|
||||||
) {
|
|
||||||
clickCallback = callback
|
|
||||||
activity.navigate(R.id.global_to_navigation_quick_search, Bundle().apply {
|
|
||||||
putBoolean("mainapi", false)
|
|
||||||
putString("autosearch", autoSearch)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var clickCallback: ((SearchClickCallback) -> Unit)? = null
|
var clickCallback: ((SearchClickCallback) -> Unit)? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,10 +76,6 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
context?.fixPaddingStatusbar(quick_search_root)
|
context?.fixPaddingStatusbar(quick_search_root)
|
||||||
|
|
||||||
arguments?.getBoolean("mainapi", true)?.let {
|
|
||||||
isMainApis = it
|
|
||||||
}
|
|
||||||
|
|
||||||
val listLock = ReentrantLock()
|
val listLock = ReentrantLock()
|
||||||
observe(searchViewModel.currentSearch) { list ->
|
observe(searchViewModel.currentSearch) { list ->
|
||||||
try {
|
try {
|
||||||
|
@ -114,44 +99,38 @@ class QuickSearchFragment(var isMainApis: Boolean = false) : Fragment() {
|
||||||
|
|
||||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||||
ParentItemAdapter(mutableListOf(), { callback ->
|
ParentItemAdapter(mutableListOf(), { callback ->
|
||||||
when (callback.action) {
|
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||||
SEARCH_ACTION_LOAD -> {
|
//when (callback.action) {
|
||||||
if (isMainApis) {
|
//SEARCH_ACTION_LOAD -> {
|
||||||
activity?.popCurrentPage()
|
// clickCallback?.invoke(callback)
|
||||||
|
//}
|
||||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
// else -> SearchHelper.handleSearchClickCallback(activity, callback)
|
||||||
} else {
|
//}
|
||||||
clickCallback?.invoke(callback)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> SearchHelper.handleSearchClickCallback(activity, callback)
|
|
||||||
}
|
|
||||||
}, { item ->
|
}, { item ->
|
||||||
activity?.loadHomepageList(item)
|
activity?.loadHomepageList(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
val searchExitIcon =
|
val searchExitIcon =
|
||||||
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||||
|
|
||||||
val searchMagIcon =
|
val searchMagIcon =
|
||||||
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||||
|
|
||||||
searchMagIcon?.scaleX = 0.65f
|
searchMagIcon?.scaleX = 0.65f
|
||||||
searchMagIcon?.scaleY = 0.65f
|
searchMagIcon?.scaleY = 0.65f
|
||||||
|
|
||||||
quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
val active = if (isMainApis) {
|
context?.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
|
||||||
val langs = context?.getApiProviderLangSettings()
|
?.map { it.name }?.toSet()?.let { active ->
|
||||||
apis.filter { langs?.contains(it.lang) == true }.map { it.name }.toSet()
|
searchViewModel.searchAndCancel(
|
||||||
} else emptySet()
|
query = query,
|
||||||
|
ignoreSettings = false,
|
||||||
searchViewModel.searchAndCancel(
|
providersActive = active
|
||||||
query = query,
|
)
|
||||||
isMainApis = isMainApis,
|
quick_search?.let {
|
||||||
ignoreSettings = false,
|
UIHelper.hideKeyboard(it)
|
||||||
providersActive = active
|
}
|
||||||
)
|
|
||||||
quick_search?.let {
|
|
||||||
UIHelper.hideKeyboard(it)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -47,8 +47,6 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.mvvm.*
|
import com.lagradost.cloudstream3.mvvm.*
|
||||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||||
|
@ -60,6 +58,7 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
|
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getDownloadSubsLanguageISO639_1
|
||||||
|
@ -73,8 +72,6 @@ import com.lagradost.cloudstream3.utils.CastHelper.startCast
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
|
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.addSync
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getSync
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||||
|
@ -93,6 +90,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.setImageBlur
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||||
import kotlinx.android.synthetic.main.fragment_result.*
|
import kotlinx.android.synthetic.main.fragment_result.*
|
||||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||||
|
import kotlinx.android.synthetic.main.result_sync.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -372,6 +370,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
private var currentLoadingCount =
|
private var currentLoadingCount =
|
||||||
0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED
|
0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED
|
||||||
private lateinit var viewModel: ResultViewModel //by activityViewModels()
|
private lateinit var viewModel: ResultViewModel //by activityViewModels()
|
||||||
|
private lateinit var syncModel: SyncViewModel
|
||||||
private var currentHeaderName: String? = null
|
private var currentHeaderName: String? = null
|
||||||
private var currentType: TvType? = null
|
private var currentType: TvType? = null
|
||||||
private var currentEpisodes: List<ResultEpisode>? = null
|
private var currentEpisodes: List<ResultEpisode>? = null
|
||||||
|
@ -384,6 +383,9 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
): View? {
|
): View? {
|
||||||
viewModel =
|
viewModel =
|
||||||
ViewModelProvider(this)[ResultViewModel::class.java]
|
ViewModelProvider(this)[ResultViewModel::class.java]
|
||||||
|
syncModel =
|
||||||
|
ViewModelProvider(this)[SyncViewModel::class.java]
|
||||||
|
|
||||||
return inflater.inflate(R.layout.fragment_result_swipe, container, false)
|
return inflater.inflate(R.layout.fragment_result_swipe, container, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -469,16 +471,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
var startAction: Int? = null
|
var startAction: Int? = null
|
||||||
private var startValue: Int? = null
|
private var startValue: Int? = null
|
||||||
|
|
||||||
private fun updateSync(id: Int) {
|
|
||||||
val syncList = getSync(id, SyncApis.map { it.idPrefix }) ?: return
|
|
||||||
val list = ArrayList<Pair<SyncAPI, String>>()
|
|
||||||
for (i in 0 until SyncApis.count()) {
|
|
||||||
val res = syncList[i] ?: continue
|
|
||||||
list.add(Pair(SyncApis[i], res))
|
|
||||||
}
|
|
||||||
viewModel.updateSync(context, list)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) {
|
private fun setFormatText(textView: TextView?, @StringRes format: Int, arg: Any?) {
|
||||||
// java.util.IllegalFormatConversionException: f != java.lang.Integer
|
// java.util.IllegalFormatConversionException: f != java.lang.Integer
|
||||||
// This can fail with malformed formatting
|
// This can fail with malformed formatting
|
||||||
|
@ -525,6 +517,16 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
|
setFormatText(result_meta_rating, R.string.rating_format, rating?.div(1000f))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setMalSync(id: String?): Boolean {
|
||||||
|
syncModel.setMalId(id ?: return false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAniListSync(id: String?): Boolean {
|
||||||
|
syncModel.setAniListId(id ?: return false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private fun setActors(actors: List<ActorData>?) {
|
private fun setActors(actors: List<ActorData>?) {
|
||||||
if (actors.isNullOrEmpty()) {
|
if (actors.isNullOrEmpty()) {
|
||||||
result_cast_text?.isVisible = false
|
result_cast_text?.isVisible = false
|
||||||
|
@ -1147,6 +1149,45 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observe(syncModel.status) { status ->
|
||||||
|
var closed = false
|
||||||
|
when (status) {
|
||||||
|
is Resource.Failure -> {
|
||||||
|
result_sync_loading_shimmer?.stopShimmer()
|
||||||
|
result_sync_loading_shimmer?.isVisible = false
|
||||||
|
result_sync_holder?.isVisible = false
|
||||||
|
}
|
||||||
|
is Resource.Loading -> {
|
||||||
|
result_sync_loading_shimmer?.startShimmer()
|
||||||
|
result_sync_loading_shimmer?.isVisible = true
|
||||||
|
result_sync_holder?.isVisible = false
|
||||||
|
}
|
||||||
|
is Resource.Success -> {
|
||||||
|
result_sync_loading_shimmer?.stopShimmer()
|
||||||
|
result_sync_loading_shimmer?.isVisible = false
|
||||||
|
result_sync_holder?.isVisible = true
|
||||||
|
|
||||||
|
val d = status.value
|
||||||
|
result_sync_rating?.value = d.score?.toFloat() ?: 0.0f
|
||||||
|
|
||||||
|
/*when(d.status) {
|
||||||
|
-1 -> None
|
||||||
|
0 -> Watching
|
||||||
|
1 -> Completed
|
||||||
|
2 -> OnHold
|
||||||
|
3 -> Dropped
|
||||||
|
4 -> PlanToWatch
|
||||||
|
5 -> ReWatching
|
||||||
|
}*/
|
||||||
|
//d.status
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
closed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
|
||||||
|
}
|
||||||
|
|
||||||
observe(viewModel.episodes) { episodeList ->
|
observe(viewModel.episodes) { episodeList ->
|
||||||
lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
|
lateFixDownloadButton(episodeList.size <= 1) // movies can have multible parts but still be *movies* this will fix this
|
||||||
var isSeriesVisible = false
|
var isSeriesVisible = false
|
||||||
|
@ -1262,7 +1303,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
observe(viewModel.dubSubSelections) { range ->
|
observe(viewModel.dubSubSelections) { range ->
|
||||||
dubRange = range
|
dubRange = range
|
||||||
|
|
||||||
if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true){
|
if (preferDub && dubRange?.contains(DubStatus.Dubbed) == true) {
|
||||||
viewModel.changeDubStatus(DubStatus.Dubbed)
|
viewModel.changeDubStatus(DubStatus.Dubbed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1297,7 +1338,7 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result_episode_select.setOnClickListener {
|
result_episode_select?.setOnClickListener {
|
||||||
val ranges = episodeRanges
|
val ranges = episodeRanges
|
||||||
if (ranges != null) {
|
if (ranges != null) {
|
||||||
it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) }
|
it.popupMenuNoIconsAndNoStringRes(ranges.mapIndexed { index, s -> Pair(index, s) }
|
||||||
|
@ -1307,6 +1348,13 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result_sync_set_score?.setOnClickListener {
|
||||||
|
// TODO set score
|
||||||
|
//syncModel.setScore(SyncAPI.SyncStatus(
|
||||||
|
// status =
|
||||||
|
//))
|
||||||
|
}
|
||||||
|
|
||||||
observe(viewModel.publicEpisodesCount) { count ->
|
observe(viewModel.publicEpisodesCount) { count ->
|
||||||
if (count < 0) {
|
if (count < 0) {
|
||||||
result_episodes_text?.isVisible = false
|
result_episodes_text?.isVisible = false
|
||||||
|
@ -1321,19 +1369,6 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
currentId = it
|
currentId = it
|
||||||
}
|
}
|
||||||
|
|
||||||
observe(viewModel.sync) { sync ->
|
|
||||||
for (s in sync) {
|
|
||||||
when (s) {
|
|
||||||
is Resource.Success -> {
|
|
||||||
val d = s.value ?: continue
|
|
||||||
setDuration(d.duration)
|
|
||||||
setRating(d.publicScore)
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observe(viewModel.resultResponse) { data ->
|
observe(viewModel.resultResponse) { data ->
|
||||||
when (data) {
|
when (data) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
|
@ -1399,27 +1434,12 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSync(d.getId())
|
|
||||||
result_add_sync?.setOnClickListener {
|
|
||||||
QuickSearchFragment.pushSync(activity, d.name) { click ->
|
|
||||||
addSync(d.getId(), click.card.apiName, click.card.url)
|
|
||||||
|
|
||||||
showToast(
|
|
||||||
activity,
|
|
||||||
context?.getString(R.string.added_sync_format)
|
|
||||||
?.format(click.card.name),
|
|
||||||
Toast.LENGTH_SHORT
|
|
||||||
)
|
|
||||||
|
|
||||||
updateSync(d.getId())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val showStatus = when (d) {
|
val showStatus = when (d) {
|
||||||
is TvSeriesLoadResponse -> d.showStatus
|
is TvSeriesLoadResponse -> d.showStatus
|
||||||
is AnimeLoadResponse -> d.showStatus
|
is AnimeLoadResponse -> d.showStatus
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
setShow(showStatus)
|
setShow(showStatus)
|
||||||
setDuration(d.duration)
|
setDuration(d.duration)
|
||||||
setYear(d.year)
|
setYear(d.year)
|
||||||
|
@ -1427,6 +1447,18 @@ class ResultFragment : Fragment(), PanelsChildGestureRegionObserver.GestureRegio
|
||||||
setRecommendations(d.recommendations)
|
setRecommendations(d.recommendations)
|
||||||
setActors(d.actors)
|
setActors(d.actors)
|
||||||
|
|
||||||
|
if (SettingsFragment.accountEnabled)
|
||||||
|
if (d is AnimeLoadResponse) {
|
||||||
|
if (
|
||||||
|
setMalSync(d.malId?.toString())
|
||||||
|
||
|
||||||
|
setAniListSync(d.anilistId?.toString())
|
||||||
|
) {
|
||||||
|
syncModel.updateMetadata()
|
||||||
|
syncModel.updateStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
result_meta_site?.text = d.apiName
|
result_meta_site?.text = d.apiName
|
||||||
|
|
||||||
val posterImageLink = d.posterUrl
|
val posterImageLink = d.posterUrl
|
||||||
|
|
|
@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.getId
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.player.IGenerator
|
import com.lagradost.cloudstream3.ui.player.IGenerator
|
||||||
|
@ -79,9 +78,6 @@ class ResultViewModel : ViewModel() {
|
||||||
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
|
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData()
|
||||||
val watchStatus: LiveData<WatchType> get() = _watchStatus
|
val watchStatus: LiveData<WatchType> get() = _watchStatus
|
||||||
|
|
||||||
private val _sync: MutableLiveData<List<Resource<SyncAPI.SyncResult?>>> = MutableLiveData()
|
|
||||||
val sync: LiveData<List<Resource<SyncAPI.SyncResult?>>> get() = _sync
|
|
||||||
|
|
||||||
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
|
fun updateWatchStatus(status: WatchType) = viewModelScope.launch {
|
||||||
val currentId = id.value ?: return@launch
|
val currentId = id.value ?: return@launch
|
||||||
_watchStatus.postValue(status)
|
_watchStatus.postValue(status)
|
||||||
|
@ -233,17 +229,6 @@ class ResultViewModel : ViewModel() {
|
||||||
return generator
|
return generator
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateSync(context: Context?, sync: List<Pair<SyncAPI, String>>) = viewModelScope.launch {
|
|
||||||
if (context == null) return@launch
|
|
||||||
|
|
||||||
val list = ArrayList<Resource<SyncAPI.SyncResult?>>()
|
|
||||||
for (s in sync) {
|
|
||||||
val result = safeApiCall { s.first.getResult(s.second) }
|
|
||||||
list.add(result)
|
|
||||||
_sync.postValue(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
|
private fun updateEpisodes(localId: Int?, list: List<ResultEpisode>, selection: Int?) {
|
||||||
_episodes.postValue(list)
|
_episodes.postValue(list)
|
||||||
generator = RepoLinkGenerator(list)
|
generator = RepoLinkGenerator(list)
|
||||||
|
|
|
@ -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.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.lagradost.cloudstream3.*
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
|
import com.lagradost.cloudstream3.apmap
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.SyncApis
|
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -39,7 +37,6 @@ class SearchViewModel : ViewModel() {
|
||||||
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory
|
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory
|
||||||
|
|
||||||
private val repos = apis.map { APIRepository(it) }
|
private val repos = apis.map { APIRepository(it) }
|
||||||
private val syncApis = SyncApis
|
|
||||||
|
|
||||||
fun clearSearch() {
|
fun clearSearch() {
|
||||||
_searchResponse.postValue(Resource.Success(ArrayList()))
|
_searchResponse.postValue(Resource.Success(ArrayList()))
|
||||||
|
@ -49,33 +46,11 @@ class SearchViewModel : ViewModel() {
|
||||||
private var onGoingSearch: Job? = null
|
private var onGoingSearch: Job? = null
|
||||||
fun searchAndCancel(
|
fun searchAndCancel(
|
||||||
query: String,
|
query: String,
|
||||||
isMainApis: Boolean = true,
|
|
||||||
providersActive: Set<String> = setOf(),
|
providersActive: Set<String> = setOf(),
|
||||||
ignoreSettings: Boolean = false
|
ignoreSettings: Boolean = false
|
||||||
) {
|
) {
|
||||||
onGoingSearch?.cancel()
|
onGoingSearch?.cancel()
|
||||||
onGoingSearch = search(query, isMainApis, providersActive, ignoreSettings)
|
onGoingSearch = search(query, providersActive, ignoreSettings)
|
||||||
}
|
|
||||||
|
|
||||||
data class SyncSearchResultSearchResponse(
|
|
||||||
override val name: String,
|
|
||||||
override val url: String,
|
|
||||||
override val apiName: String,
|
|
||||||
override var type: TvType?,
|
|
||||||
override var posterUrl: String?,
|
|
||||||
override var id: Int?,
|
|
||||||
override var quality: SearchQuality? = null
|
|
||||||
) : SearchResponse
|
|
||||||
|
|
||||||
private fun SyncAPI.SyncSearchResult.toSearchResponse(): SyncSearchResultSearchResponse {
|
|
||||||
return SyncSearchResultSearchResponse(
|
|
||||||
this.name,
|
|
||||||
this.url,
|
|
||||||
this.syncApiName,
|
|
||||||
null,
|
|
||||||
this.posterUrl,
|
|
||||||
null, //this.id.hashCode()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateHistory() = viewModelScope.launch {
|
fun updateHistory() = viewModelScope.launch {
|
||||||
|
@ -89,7 +64,6 @@ class SearchViewModel : ViewModel() {
|
||||||
|
|
||||||
private fun search(
|
private fun search(
|
||||||
query: String,
|
query: String,
|
||||||
isMainApis: Boolean = true,
|
|
||||||
providersActive: Set<String>,
|
providersActive: Set<String>,
|
||||||
ignoreSettings: Boolean = false
|
ignoreSettings: Boolean = false
|
||||||
) =
|
) =
|
||||||
|
@ -118,23 +92,12 @@ class SearchViewModel : ViewModel() {
|
||||||
_currentSearch.postValue(ArrayList())
|
_currentSearch.postValue(ArrayList())
|
||||||
|
|
||||||
withContext(Dispatchers.IO) { // This interrupts UI otherwise
|
withContext(Dispatchers.IO) { // This interrupts UI otherwise
|
||||||
if (isMainApis) {
|
repos.filter { a ->
|
||||||
repos.filter { a ->
|
ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))
|
||||||
ignoreSettings || (providersActive.isEmpty() || providersActive.contains(a.name))
|
}.apmap { a -> // Parallel
|
||||||
}.apmap { a -> // Parallel
|
val search = a.search(query)
|
||||||
val search = a.search(query)
|
currentList.add(OnGoingSearch(a.name, search))
|
||||||
currentList.add(OnGoingSearch(a.name, search))
|
_currentSearch.postValue(currentList)
|
||||||
_currentSearch.postValue(currentList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
syncApis.apmap { a ->
|
|
||||||
val search = safeApiCall {
|
|
||||||
a.search(query)?.map { it.toSearchResponse() }
|
|
||||||
?: throw ErrorLoadingException()
|
|
||||||
}
|
|
||||||
|
|
||||||
currentList.add(OnGoingSearch(a.name, search))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_currentSearch.postValue(currentList)
|
_currentSearch.postValue(currentList)
|
||||||
|
|
|
@ -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
|
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val accountEnabled = false
|
const val accountEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var beneneCount = 0
|
private var beneneCount = 0
|
||||||
|
@ -155,7 +155,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val dialog = builder.show()
|
val dialog = builder.show()
|
||||||
|
|
||||||
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
|
dialog.findViewById<TextView>(R.id.account_add)?.setOnClickListener {
|
||||||
api.authenticate()
|
try {
|
||||||
|
api.authenticate()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ogIndex = api.accountIndex
|
val ogIndex = api.accountIndex
|
||||||
|
@ -325,10 +329,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
val syncApis =
|
val syncApis =
|
||||||
listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
|
listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
|
||||||
for (sync in syncApis) {
|
for ((key, api) in syncApis) {
|
||||||
getPref(sync.first)?.apply {
|
getPref(key)?.apply {
|
||||||
isVisible = accountEnabled
|
isVisible = accountEnabled
|
||||||
val api = sync.second
|
|
||||||
title =
|
title =
|
||||||
getString(R.string.login_format).format(api.name, getString(R.string.account))
|
getString(R.string.login_format).format(api.name, getString(R.string.account))
|
||||||
setOnPreferenceClickListener { _ ->
|
setOnPreferenceClickListener { _ ->
|
||||||
|
@ -336,7 +339,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
showLoginInfo(api, info)
|
showLoginInfo(api, info)
|
||||||
} else {
|
} else {
|
||||||
api.authenticate()
|
try {
|
||||||
|
api.authenticate()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return@setOnPreferenceClickListener true
|
return@setOnPreferenceClickListener true
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -23,6 +22,7 @@ import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.tvprovider.media.tv.PreviewChannelHelper
|
import androidx.tvprovider.media.tv.PreviewChannelHelper
|
||||||
import androidx.tvprovider.media.tv.TvContractCompat
|
import androidx.tvprovider.media.tv.TvContractCompat
|
||||||
import androidx.tvprovider.media.tv.WatchNextProgram
|
import androidx.tvprovider.media.tv.WatchNextProgram
|
||||||
|
@ -33,7 +33,10 @@ import com.google.android.gms.cast.framework.CastState
|
||||||
import com.google.android.gms.common.ConnectionResult
|
import com.google.android.gms.common.ConnectionResult
|
||||||
import com.google.android.gms.common.GoogleApiAvailability
|
import com.google.android.gms.common.GoogleApiAvailability
|
||||||
import com.google.android.gms.common.wrappers.Wrappers
|
import com.google.android.gms.common.wrappers.Wrappers
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.R
|
||||||
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
|
import com.lagradost.cloudstream3.isMovieType
|
||||||
|
import com.lagradost.cloudstream3.mapper
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
@ -196,17 +199,10 @@ object AppUtils {
|
||||||
|
|
||||||
fun Context.openBrowser(url: String) {
|
fun Context.openBrowser(url: String) {
|
||||||
try {
|
try {
|
||||||
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW)
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
intent.data = Uri.parse(url)
|
intent.data = Uri.parse(url)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
ContextCompat.startActivity(this, intent, null)
|
||||||
startActivity(
|
|
||||||
Intent.createChooser(intent, null)
|
|
||||||
.putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components)
|
|
||||||
)
|
|
||||||
else
|
|
||||||
startActivity(intent)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:visibility="gone"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:id="@+id/sync_holder"
|
android:id="@+id/result_sync_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
@ -38,8 +39,9 @@
|
||||||
app:tint="?attr/textColor" />
|
app:tint="?attr/textColor" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
|
android:id="@+id/result_sync_current_episodes"
|
||||||
style="@style/AppEditStyle"
|
style="@style/AppEditStyle"
|
||||||
android:hint="20"
|
tools:hint="20"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:inputType="number"
|
android:inputType="number"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -47,11 +49,12 @@
|
||||||
tools:ignore="LabelFor" />
|
tools:ignore="LabelFor" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/result_sync_max_episodes"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:paddingBottom="1dp"
|
android:paddingBottom="1dp"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textColor="?attr/textColor"
|
android:textColor="?attr/textColor"
|
||||||
android:text="/30"
|
tools:text="/30"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
@ -64,10 +67,10 @@
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
android:contentDescription="@string/result_share"
|
android:contentDescription="@string/result_share"
|
||||||
app:tint="?attr/textColor" />
|
app:tint="?attr/textColor" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.core.widget.ContentLoadingProgressBar
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
|
android:id="@+id/result_sync_episodes"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
|
@ -192,6 +195,7 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.slider.Slider
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/result_sync_rating"
|
||||||
android:valueFrom="0"
|
android:valueFrom="0"
|
||||||
android:valueTo="10"
|
android:valueTo="10"
|
||||||
android:value="4"
|
android:value="4"
|
||||||
|
@ -350,6 +354,7 @@
|
||||||
style="@style/WhiteButton"
|
style="@style/WhiteButton"
|
||||||
android:text="@string/type_watching" />
|
android:text="@string/type_watching" />
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/result_sync_set_score"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
style="@style/BlackButton"
|
style="@style/BlackButton"
|
||||||
|
@ -357,13 +362,48 @@
|
||||||
android:text="@string/upload_sync" />
|
android:text="@string/upload_sync" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
<com.google.android.material.button.MaterialButton
|
android:id="@+id/result_sync_loading_shimmer"
|
||||||
android:visibility="gone"
|
app:shimmer_base_alpha="0.2"
|
||||||
android:id="@+id/sync_add_tracking"
|
app:shimmer_highlight_alpha="0.3"
|
||||||
android:layout_gravity="center"
|
app:shimmer_duration="@integer/loading_time"
|
||||||
|
app:shimmer_auto_start="true"
|
||||||
|
android:padding="15dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
style="@style/SyncButton"
|
android:layout_height="match_parent"
|
||||||
app:icon="@drawable/ic_baseline_add_24"
|
android:layout_gravity="center"
|
||||||
android:text="@string/add_sync" />
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="30dp"/>
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
|
||||||
|
<include layout="@layout/loading_line" />
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="30dp"/>
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
<include layout="@layout/loading_line" />
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="30dp"/>
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
<include layout="@layout/loading_line_short" />
|
||||||
|
<Space
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="30dp"/>
|
||||||
|
<include layout="@layout/loading_line" />
|
||||||
|
<include layout="@layout/loading_line" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
@ -9,9 +10,7 @@ import org.junit.Test
|
||||||
|
|
||||||
class ProviderTests {
|
class ProviderTests {
|
||||||
private fun getAllProviders(): List<MainAPI> {
|
private fun getAllProviders(): List<MainAPI> {
|
||||||
val allApis = APIHolder.apis
|
return allProviders.filter { !it.usesWebView }
|
||||||
allApis.addAll(APIHolder.restrictedApis)
|
|
||||||
return allApis.filter { !it.usesWebView }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
|
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
|
||||||
|
|
Loading…
Reference in a new issue