updated syncapi and possible bug fixed

This commit is contained in:
LagradOst 2021-11-12 17:55:54 +01:00
parent 1628ec56c2
commit a16cd3ef9a
17 changed files with 710 additions and 398 deletions

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#6A32D3</color>
<!--#6A32D3-->
<color name="ic_launcher_background">@color/colorPrimaryPurple</color>
</resources>

View file

@ -54,7 +54,7 @@ object APIHolder {
)
val restrictedApis = arrayListOf(
TrailersToProvider(), // be aware that this is fuckery
// TrailersToProvider(), // be aware that this is fuckery
// NyaaProvider(), // torrents in cs3 is wack
// ThenosProvider(), // ddos protection and wacked links
AsiaFlixProvider(),
@ -314,7 +314,7 @@ interface SearchResponse {
val name: String
val url: String
val apiName: String
val type: TvType
val type: TvType?
val posterUrl: String?
val id: Int?
}

View file

@ -28,9 +28,9 @@ import com.lagradost.cloudstream3.APIHolder.restrictedApis
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initRequestClient
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.OAuth2accountApis
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.player.PlayerEventType
@ -45,6 +45,7 @@ import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
@ -447,6 +448,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view.setupWithNavController(navController)
navController.addOnDestinationChangedListener { _, destination, _ ->
this.hideKeyboard()
// nav_view.hideKeyboard()
/*if (destination.id != R.id.navigation_player) {
requestedOrientation = if (settingsManager?.getBoolean("force_landscape", false) == true) {

View file

@ -0,0 +1,62 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
abstract class AccountManager(private val defIndex: Int) : OAuth2API {
// don't change this as all keys depend on it
open val idPrefix: String
get() {
throw(NotImplementedError())
}
var accountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active"
// int array of all accounts indexes
private val accountsKey get() = "${idPrefix}_accounts"
protected fun Context.removeAccountKeys() {
this.removeKeys(accountId)
val accounts = getAccounts(this).toMutableList()
accounts.remove(accountIndex)
this.setKey(accountsKey, accounts.toIntArray())
init(this)
}
fun getAccounts(context: Context): IntArray {
return context.getKey(accountsKey, intArrayOf())!!
}
fun init(context: Context) {
accountIndex = context.getKey(accountActiveKey, defIndex)!!
val accounts = getAccounts(context)
if (accounts.isNotEmpty() && this.loginInfo(context) == null) {
accountIndex = accounts.first()
}
}
protected fun Context.switchToNewAccount() {
val accounts = getAccounts(this)
accountIndex = (accounts.maxOrNull() ?: 0) + 1
}
protected fun Context.registerAccount() {
this.setKey(accountActiveKey, accountIndex)
val accounts = getAccounts(this).toMutableList()
if (!accounts.contains(accountIndex)) {
accounts.add(accountIndex)
}
this.setKey(accountsKey, accounts.toIntArray())
}
fun changeAccount(context: Context, index: Int) {
accountIndex = index
context.setKey(accountActiveKey, index)
}
}

View file

@ -0,0 +1,75 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import java.util.concurrent.TimeUnit
interface OAuth2API {
val key: String
val name: String
val redirectUrl: String
fun handleRedirect(context: Context, url: String)
fun authenticate(context: Context)
fun loginInfo(context: Context): LoginInfo?
fun logOut(context: Context)
class LoginInfo(
val profilePicture: String?,
val name: String?,
val accountIndex: Int,
)
companion object {
val malApi = MALApi(0)
val aniListApi = AniListApi(0)
// used to login via app intent
val OAuth2Apis
get() = listOf<OAuth2API>(
malApi, aniListApi
)
// this needs init with context and can be accessed in settings
val OAuth2accountApis
get() = listOf<AccountManager>(
malApi, aniListApi
)
// used for active syncing
val SyncApis
get() = listOf<SyncAPI>(
malApi, aniListApi
)
const val appString = "cloudstreamapp"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
const val maxStale = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
val days = TimeUnit.SECONDS
.toDays(secondsLong)
secondsLong -= TimeUnit.DAYS.toSeconds(days)
val hours = TimeUnit.SECONDS
.toHours(secondsLong)
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
val minutes = TimeUnit.SECONDS
.toMinutes(secondsLong)
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
if (minutes < 0) {
return completedValue
}
//println("$days $hours $minutes")
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
}
}
}

View file

@ -1,127 +0,0 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import java.util.concurrent.TimeUnit
interface OAuth2Interface {
val key: String
val name: String
val redirectUrl: String
fun handleRedirect(context: Context, url: String)
fun authenticate(context: Context)
fun loginInfo(context: Context): LoginInfo?
fun logOut(context: Context)
class LoginInfo(
val profilePicture: String?,
val name: String?,
val accountIndex: Int,
)
abstract class AccountManager(private val defIndex: Int) : OAuth2Interface {
// don't change this as all keys depend on it
open val idPrefix: String
get() {
throw(NotImplementedError())
}
var accountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active"
// int array of all accounts indexes
private val accountsKey get() = "${idPrefix}_accounts"
protected fun Context.removeAccountKeys() {
this.removeKeys(accountId)
val accounts = getAccounts(this).toMutableList()
accounts.remove(accountIndex)
this.setKey(accountsKey, accounts.toIntArray())
init(this)
}
fun getAccounts(context: Context): IntArray {
return context.getKey(accountsKey, intArrayOf())!!
}
fun init(context: Context) {
accountIndex = context.getKey(accountActiveKey, defIndex)!!
val accounts = getAccounts(context)
if (accounts.isNotEmpty() && this.loginInfo(context) == null) {
accountIndex = accounts.first()
}
}
protected fun Context.switchToNewAccount() {
val accounts = getAccounts(this)
accountIndex = (accounts.maxOrNull() ?: 0) + 1
}
protected fun Context.registerAccount() {
this.setKey(accountActiveKey, accountIndex)
val accounts = getAccounts(this).toMutableList()
if (!accounts.contains(accountIndex)) {
accounts.add(accountIndex)
}
this.setKey(accountsKey, accounts.toIntArray())
}
fun changeAccount(context: Context, index: Int) {
accountIndex = index
context.setKey(accountActiveKey, index)
}
}
companion object {
val malApi = MALApi(0)
val aniListApi = AniListApi(0)
val OAuth2Apis
get() = listOf<OAuth2Interface>(
malApi, aniListApi
)
// this needs init with context
val OAuth2accountApis
get() = listOf<AccountManager>(
malApi, aniListApi
)
const val appString = "cloudstreamapp"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMS: Long
get() = System.currentTimeMillis()
const val maxStale = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
val days = TimeUnit.SECONDS
.toDays(secondsLong)
secondsLong -= TimeUnit.DAYS.toSeconds(days)
val hours = TimeUnit.SECONDS
.toHours(secondsLong)
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
val minutes = TimeUnit.SECONDS
.toMinutes(secondsLong)
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
if (minutes < 0) {
return completedValue
}
//println("$days $hours $minutes")
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
}
}
}

View file

@ -0,0 +1,86 @@
package com.lagradost.cloudstream3.syncproviders
import android.content.Context
import com.lagradost.cloudstream3.ShowStatus
interface SyncAPI {
data class SyncSearchResult(
val name: String,
val syncApiName: String,
val id: String,
val url: String?,
val posterUrl: String?,
)
data class SyncNextAiring(
val episode: Int,
val unixTime: Long,
)
data class SyncActor(
val name: String,
val posterUrl: String?,
)
data class SyncCharacter(
val name: String,
val posterUrl: String?,
)
data class SyncStatus(
val status: Int,
/** 1-10 */
val score: Int?,
val watchedEpisodes: Int?,
var isFavorite: Boolean? = null,
)
data class SyncResult(
/**Used to verify*/
var id: String,
var totalEpisodes: Int? = null,
var title: String? = null,
/**1-1000*/
var publicScore: Int? = null,
/**In minutes*/
var duration: Int? = null,
var synopsis: String? = null,
var airStatus: ShowStatus? = null,
var nextAiring: SyncNextAiring? = null,
var studio: String? = null,
var genres: List<String>? = null,
var trailerUrl: String? = null,
/** In unixtime */
var startDate: Long? = null,
/** In unixtime */
var endDate: Long? = null,
var recommendations: List<SyncSearchResult>? = null,
var nextSeason: SyncSearchResult? = null,
var prevSeason: SyncSearchResult? = null,
var actors: List<SyncActor>? = null,
var characters: List<SyncCharacter>? = null,
)
val icon : Int
val mainUrl: String
fun search(context: Context, name: String): List<SyncSearchResult>?
/**
-1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
*/
fun score(context: Context, id: String, status : SyncStatus): Boolean
fun getStatus(context: Context, id : String) : SyncStatus?
fun getResult(context: Context, id: String): SyncResult?
}

View file

@ -1,4 +1,4 @@
package com.lagradost.cloudstream3.syncproviders
package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty
@ -6,12 +6,16 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.post
import com.lagradost.cloudstream3.network.text
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.maxStale
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.maxStale
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -21,9 +25,8 @@ import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL
import java.util.*
import java.util.concurrent.TimeUnit
class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val name: String
get() = "AniList"
override val key: String
@ -32,11 +35,19 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
get() = "anilistlogin"
override val idPrefix: String
get() = "anilist"
override val mainUrl: String
get() = "https://anilist.co"
override val icon: Int
get() = R.drawable.ic_anilist_icon
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? {
override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
// context.getUser(true)?.
context.getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
return OAuth2Interface.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex)
return OAuth2API.LoginInfo(
profilePicture = user.picture,
name = user.name,
accountIndex = accountIndex
)
}
return null
}
@ -71,7 +82,60 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
}
}
override fun search(context: Context, name: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null
return data.data.Page.media.map {
SyncAPI.SyncSearchResult(
it.title.romaji,
this.name,
it.id.toString(),
"$mainUrl/anime/${it.id}",
it.bannerImage
)
}
}
override fun getResult(context: Context, id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null
val season = getSeason(internalId)?.data?.Media ?: return null
return SyncAPI.SyncResult(
season.id.toString(),
nextAiring = season.nextAiringEpisode?.let {
SyncAPI.SyncNextAiring(
it.episode,
it.timeUntilAiring + unixTime
)
},
//TODO REST
)
}
override fun getStatus(context: Context, id: String): SyncAPI.SyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = context.getDataAboutId(internalId) ?: return null
return SyncAPI.SyncStatus(
score = data.score,
watchedEpisodes = data.episodes,
status = data.type.value,
isFavorite = data.isFavourite,
)
}
override fun score(context: Context, id: String, status: SyncAPI.SyncStatus): Boolean {
return context.postDataAboutId(
id.toIntOrNull() ?: return false,
fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
)
}
companion object {
private val mapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
private val aniListStatusString = arrayOf("CURRENT", "COMPLETED", "PAUSED", "DROPPED", "PLANNING", "REPEATING")
const val ANILIST_UNIXTIME_KEY: String = "anilist_unixtime" // When token expires
@ -79,81 +143,6 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list"
}
private val mapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
// Changing names of these will show up in UI
enum class AniListStatusType(var value: Int) {
Watching(0),
Completed(1),
Paused(2),
Dropped(3),
Planning(4),
Rewatching(5),
None(-1)
}
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> AniListStatusType.None
0 -> AniListStatusType.Watching
1 -> AniListStatusType.Completed
2 -> AniListStatusType.Paused
3 -> AniListStatusType.Dropped
4 -> AniListStatusType.Planning
5 -> AniListStatusType.Rewatching
else -> AniListStatusType.None
}
}
fun convertAnilistStringToStatus(string: String): AniListStatusType {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
}
fun Context.initGetUser() {
if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return
ioSafe {
getUser()
}
}
private fun Context.checkToken(): Boolean {
if (unixTime > getKey(accountId,
ANILIST_UNIXTIME_KEY, 0L
)!!
) {
/*getCurrentActivity()?.runOnUiThread {
val alertDialog: AlertDialog? = activity?.let {
val builder = AlertDialog.Builder(it, R.style.AlertDialogCustom)
builder.apply {
setPositiveButton(
"Login"
) { dialog, id ->
authenticateAniList()
}
setNegativeButton(
"Cancel"
) { dialog, id ->
// User cancelled the dialog
}
}
// Set other dialog properties
builder.setTitle("AniList token has expired")
// Create the AlertDialog
builder.create()
}
alertDialog?.show()
}*/
return true
} else {
return false
}
}
private fun fixName(name: String): String {
return name.toLowerCase(Locale.ROOT).replace(" ", "").replace("[^a-zA-Z0-9]".toRegex(), "")
@ -217,7 +206,16 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
}
"""
val data =
mapOf("query" to query, "variables" to mapper.writeValueAsString(mapOf("search" to name, "page" to 1, "type" to "ANIME")) )
mapOf(
"query" to query,
"variables" to mapper.writeValueAsString(
mapOf(
"search" to name,
"page" to 1,
"type" to "ANIME"
)
)
)
val res = post(
"https://graphql.anilist.co/",
@ -267,40 +265,120 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
return filtered?.firstOrNull()
}
private fun Context.postApi(url: String, q: String, cache: Boolean = false): String {
return try {
if (!checkToken()) {
// println("VARS_ " + vars)
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 {
""
// Changing names of these will show up in UI
enum class AniListStatusType(var value: Int) {
Watching(0),
Completed(1),
Paused(2),
Dropped(3),
Planning(4),
Rewatching(5),
None(-1)
}
} catch (e: Exception) {
logError(e)
""
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> AniListStatusType.None
0 -> AniListStatusType.Watching
1 -> AniListStatusType.Completed
2 -> AniListStatusType.Paused
3 -> AniListStatusType.Dropped
4 -> AniListStatusType.Planning
5 -> AniListStatusType.Rewatching
else -> AniListStatusType.None
}
}
data class MediaRecommendation(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: Title,
@JsonProperty("idMal") val idMal: Int?,
@JsonProperty("coverImage") val coverImage: CoverImage,
@JsonProperty("averageScore") val averageScore: Int?
)
fun convertAnilistStringToStatus(string: String): AniListStatusType {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
}
private fun getSeason(id: Int): SeasonResponse? {
val q: String = """
query (${'$'}id: Int = $id) {
Media (id: ${'$'}id, type: ANIME) {
id
idMal
relations {
edges {
id
relationType(version: 2)
node {
id
format
nextAiringEpisode {
timeUntilAiring
episode
}
}
}
}
nextAiringEpisode {
timeUntilAiring
episode
}
format
}
}
"""
val data = post(
"https://graphql.anilist.co",
data = mapOf("query" to q),
cacheTime = 0,
).text
if (data == "") return null
return try {
mapper.readValue(data)
} catch (e: Exception) {
logError(e)
null
}
}
}
fun Context.initGetUser() {
if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return
ioSafe {
getUser()
}
}
private fun Context.checkToken(): Boolean {
if (unixTime > getKey(
accountId,
ANILIST_UNIXTIME_KEY, 0L
)!!
) {
/*getCurrentActivity()?.runOnUiThread {
val alertDialog: AlertDialog? = activity?.let {
val builder = AlertDialog.Builder(it, R.style.AlertDialogCustom)
builder.apply {
setPositiveButton(
"Login"
) { dialog, id ->
authenticateAniList()
}
setNegativeButton(
"Cancel"
) { dialog, id ->
// User cancelled the dialog
}
}
// Set other dialog properties
builder.setTitle("AniList token has expired")
// Create the AlertDialog
builder.create()
}
alertDialog?.show()
}*/
return true
} else {
return false
}
}
fun Context.getDataAboutId(id: Int): AniListTitleHolder? {
val q =
@ -361,6 +439,41 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
}
}
private fun Context.postApi(url: String, q: String, cache: Boolean = false): String {
return try {
if (!checkToken()) {
// println("VARS_ " + vars)
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)
""
}
}
data class MediaRecommendation(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: Title,
@JsonProperty("idMal") val idMal: Int?,
@JsonProperty("coverImage") val coverImage: CoverImage,
@JsonProperty("averageScore") val averageScore: Int?
)
data class FullAnilistList(
@JsonProperty("data") val data: Data
)
@ -530,7 +643,7 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
return data != ""
}
fun Context.postDataAboutId(id: Int, type: AniListStatusType, score: Int, progress: Int): Boolean {
private fun Context.postDataAboutId(id: Int, type: AniListStatusType, score: Int?, progress: Int?): Boolean {
try {
val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
@ -538,7 +651,7 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
0,
type.value
)]
}, ${'$'}scoreRaw: Int = ${score * 10}, ${'$'}progress: Int = $progress) {
}, ${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
@ -597,49 +710,6 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
}
}
private fun getSeason(id: Int): SeasonResponse? {
val q: String = """
query (${'$'}id: Int = $id) {
Media (id: ${'$'}id, type: ANIME) {
id
idMal
relations {
edges {
id
relationType(version: 2)
node {
id
format
nextAiringEpisode {
timeUntilAiring
episode
}
}
}
}
nextAiringEpisode {
timeUntilAiring
episode
}
format
}
}
"""
val data = post(
"https://graphql.anilist.co",
data = mapOf("query" to q),
cacheTime = 0,
).text
if (data == "") return null
return try {
mapper.readValue(data)
} catch (e: Exception) {
logError(e)
null
}
}
fun getAllSeasons(id: Int): List<SeasonResponse?> {
val seasons = mutableListOf<SeasonResponse?>()
fun getSeasonRecursive(id: Int) {
@ -662,27 +732,6 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
return seasons.toList()
}
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
val days = TimeUnit.SECONDS
.toDays(secondsLong)
secondsLong -= TimeUnit.DAYS.toSeconds(days)
val hours = TimeUnit.SECONDS
.toHours(secondsLong)
secondsLong -= TimeUnit.HOURS.toSeconds(hours)
val minutes = TimeUnit.SECONDS
.toMinutes(secondsLong)
secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
if (minutes < 0) {
return completedValue
}
//println("$days $hours $minutes")
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
}
data class SeasonResponse(
@JsonProperty("data") val data: SeasonData,
)
@ -725,9 +774,9 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
data class SeasonNode(
@JsonProperty("id") val id: Int,
@JsonProperty("format") val format: String?,
@JsonProperty("title") val title: AniListApi.Title,
@JsonProperty("title") val title: Title,
@JsonProperty("idMal") val idMal: Int?,
@JsonProperty("coverImage") val coverImage: AniListApi.CoverImage,
@JsonProperty("coverImage") val coverImage: CoverImage,
@JsonProperty("averageScore") val averageScore: Int?
// @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
)

View file

@ -1,9 +1,10 @@
package com.lagradost.cloudstream3.syncproviders
package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync
class Dropbox : OAuth2Interface {
class Dropbox : OAuth2API {
override val name: String
get() = "Dropbox"
override val key: String
@ -23,7 +24,7 @@ class Dropbox : OAuth2Interface {
TODO("Not yet implemented")
}
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? {
override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
TODO("Not yet implemented")
}
}

View file

@ -1,4 +1,4 @@
package com.lagradost.cloudstream3.syncproviders
package com.lagradost.cloudstream3.syncproviders.providers
import android.content.Context
import android.util.Base64
@ -7,14 +7,18 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.get
import com.lagradost.cloudstream3.network.post
import com.lagradost.cloudstream3.network.put
import com.lagradost.cloudstream3.network.text
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.secondsToReadable
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.secondsToReadable
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.unixTime
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -27,7 +31,10 @@ import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
const val MAL_MAX_SEARCH_LIMIT = 25
class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val name: String
get() = "MAL"
override val key: String
@ -36,19 +43,70 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
get() = "mallogin"
override val idPrefix: String
get() = "mal"
override val mainUrl: String
get() = "https://myanimelist.net"
override val icon: Int
get() = R.drawable.mal_logo
override fun logOut(context: Context) {
context.removeAccountKeys()
}
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? {
override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
//context.getMalUser(true)?
context.getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
return OAuth2Interface.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex)
return OAuth2API.LoginInfo(profilePicture = user.picture, name = user.name, accountIndex = accountIndex)
}
return null
}
override fun search(context: Context, name: String): List<SyncAPI.SyncSearchResult> {
val url = "https://api.myanimelist.net/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val res = get(
url, headers = mapOf(
"Authorization" to "Bearer " + context.getKey<String>(
accountId,
MAL_TOKEN_KEY
)!!,
), cacheTime = 0
).text
return mapper.readValue<MalSearch>(res).data.map {
SyncAPI.SyncSearchResult(
it.title,
this.name,
it.id.toString(),
"$mainUrl/anime/${it.id}/",
it.main_picture?.large ?: it.main_picture?.medium
)
}
}
override fun score(context: Context, id: String, status : SyncAPI.SyncStatus): Boolean {
return context.setScoreRequest(
id.toIntOrNull() ?: return false,
fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
)
}
override fun getResult(context: Context, id: String): SyncAPI.SyncResult? {
val internalId = id.toIntOrNull() ?: return null
TODO("Not yet implemented")
}
override fun getStatus(context: Context, id: String): SyncAPI.SyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = context.getDataAboutMalId(internalId)?.my_list_status ?: return null
return SyncAPI.SyncStatus(
score = data.score,
status = malStatusAsString.indexOf(data.status),
isFavorite = null,
watchedEpisodes = data.num_episodes_watched,
)
}
companion object {
private val malStatusAsString = arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch")
@ -293,7 +351,7 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
}
}
fun Context.getDataAboutMalId(id: Int): MalAnime? {
private fun Context.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"
@ -419,7 +477,7 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
None(-1)
}
fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
@ -534,12 +592,23 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
@JsonProperty("picture") val picture: String,
)
data class MalMainPicture(
@JsonProperty("large") val large: String?,
@JsonProperty("medium") val medium: String?,
)
// Used for getDataAboutId()
data class MalAnime(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String,
@JsonProperty("num_episodes") val num_episodes: Int,
@JsonProperty("my_list_status") val my_list_status: MalStatus?
@JsonProperty("my_list_status") val my_list_status: MalStatus?,
@JsonProperty("main_picture") val main_picture: MalMainPicture?,
)
data class MalSearch(
@JsonProperty("data") val data: List<MalAnime>,
//paging
)
data class MalTitleHolder(

View file

@ -26,7 +26,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
@ -430,7 +430,7 @@ class HomeFragment : Fragment() {
// nice profile pic on homepage
home_profile_picture_holder?.isVisible = false
context?.let { ctx ->
for (syncApi in OAuth2Interface.OAuth2Apis) {
for (syncApi in OAuth2API.OAuth2Apis) {
val login = syncApi.loginInfo(ctx)
val pic = login?.profilePicture
if (pic != null) {

View file

@ -8,13 +8,13 @@ import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.utils.UIHelper.setImage
class AccountClickCallback(val action: Int, val view : View, val card: OAuth2Interface.LoginInfo)
class AccountClickCallback(val action: Int, val view : View, val card: OAuth2API.LoginInfo)
class AccountAdapter(
val cardList: List<OAuth2Interface.LoginInfo>,
val cardList: List<OAuth2API.LoginInfo>,
val layout: Int = R.layout.account_single,
private val clickCallback: (AccountClickCallback) -> Unit
) :
@ -48,7 +48,7 @@ class AccountAdapter(
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
fun bind(card: OAuth2Interface.LoginInfo) {
fun bind(card: OAuth2API.LoginInfo) {
// just in case name is null account index will show, should never happened
accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex)
if(card.profilePicture.isNullOrEmpty()) {

View file

@ -30,9 +30,10 @@ import com.lagradost.cloudstream3.MainActivity.Companion.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initRequestClient
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils
@ -120,7 +121,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
Triple("🇹🇷", "Turkish", "tr")
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top
private fun showAccountSwitch(context: Context, api: OAuth2Interface.AccountManager) {
private fun showAccountSwitch(context: Context, api: AccountManager) {
val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
val dialog = builder.show()
@ -132,7 +133,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val ogIndex = api.accountIndex
val items = ArrayList<OAuth2Interface.LoginInfo>()
val items = ArrayList<OAuth2API.LoginInfo>()
for (index in accounts) {
api.accountIndex = index
@ -150,7 +151,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
list?.adapter = adapter
}
private fun showLoginInfo(context: Context, api: OAuth2Interface.AccountManager, info: OAuth2Interface.LoginInfo) {
private fun showLoginInfo(context: Context, api: AccountManager, info: OAuth2API.LoginInfo) {
val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_managment)
val dialog = builder.show()

View file

@ -0,0 +1,86 @@
package com.lagradost.cloudstream3.utils
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.get
import com.lagradost.cloudstream3.network.text
import java.util.concurrent.TimeUnit
object SyncUtil {
/** first. Mal, second. Anilist,
* valid sites are: Gogoanime, Twistmoe and 9anime*/
fun getIdsFromSlug(slug: String, site : String = "Gogoanime"): Pair<String?, String?>? {
try {
//Gogoanime, Twistmoe and 9anime
val url =
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json"
val response = get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
val mapped = mapper.readValue<MalSyncPage?>(response)
val overrideMal = mapped?.malId ?: mapped?.Mal?.id ?: mapped?.Anilist?.malId
val overrideAnilist = mapped?.aniId ?: mapped?.Anilist?.id
if (overrideMal != null) {
return overrideMal.toString() to overrideAnilist?.toString()
}
return null
} catch (e: Exception) {
logError(e)
}
return null
}
data class MalSyncPage(
@JsonProperty("identifier") val identifier: String?,
@JsonProperty("type") val type: String?,
@JsonProperty("page") val page: String?,
@JsonProperty("title") val title: String?,
@JsonProperty("url") val url: String?,
@JsonProperty("image") val image: String?,
@JsonProperty("hentai") val hentai: Boolean?,
@JsonProperty("sticky") val sticky: Boolean?,
@JsonProperty("active") val active: Boolean?,
@JsonProperty("actor") val actor: String?,
@JsonProperty("malId") val malId: Int?,
@JsonProperty("aniId") val aniId: Int?,
@JsonProperty("createdAt") val createdAt: String?,
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("deletedAt") val deletedAt: String?,
@JsonProperty("Mal") val Mal: Mal?,
@JsonProperty("Anilist") val Anilist: Anilist?,
@JsonProperty("malUrl") val malUrl: String?
)
data class Anilist(
// @JsonProperty("altTitle") val altTitle: List<String>?,
// @JsonProperty("externalLinks") val externalLinks: List<String>?,
@JsonProperty("id") val id: Int?,
@JsonProperty("malId") val malId: Int?,
@JsonProperty("type") val type: String?,
@JsonProperty("title") val title: String?,
@JsonProperty("url") val url: String?,
@JsonProperty("image") val image: String?,
@JsonProperty("category") val category: String?,
@JsonProperty("hentai") val hentai: Boolean?,
@JsonProperty("createdAt") val createdAt: String?,
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("deletedAt") val deletedAt: String?
)
data class Mal(
// @JsonProperty("altTitle") val altTitle: List<String>?,
@JsonProperty("id") val id: Int?,
@JsonProperty("type") val type: String?,
@JsonProperty("title") val title: String?,
@JsonProperty("url") val url: String?,
@JsonProperty("image") val image: String?,
@JsonProperty("category") val category: String?,
@JsonProperty("hentai") val hentai: Boolean?,
@JsonProperty("createdAt") val createdAt: String?,
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("deletedAt") val deletedAt: String?
)
}

View file

@ -67,11 +67,16 @@ object UIHelper {
fun Fragment.hideKeyboard() {
activity?.window?.decorView?.clearFocus()
view.let {
if (it != null) {
view?.let {
hideKeyboard(it)
}
}
fun Activity.hideKeyboard() {
window?.decorView?.clearFocus()
this.findViewById<View>(android.R.id.content)?.rootView?.let {
hideKeyboard(it)
}
}
fun Activity?.navigate(@IdRes navigation: Int, arguments: Bundle? = null) {

View file

@ -84,6 +84,7 @@
<string name="type_dropped">Dropped</string>
<string name="type_plan_to_watch">Plan to Watch</string>
<string name="type_none">None</string>
<string name="type_re_watching">ReWatching</string>
<string name="play_movie_button">Play Movie</string>
<string name="play_torrent_button">Stream Torrent</string>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#3DDC84</color>
<!--#3DDC84-->
<color name="ic_launcher_background">@color/colorPrimaryDark</color>
</resources>