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"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="ic_launcher_background">#6A32D3</color> <!--#6A32D3-->
<color name="ic_launcher_background">@color/colorPrimaryPurple</color>
</resources> </resources>

View file

@ -54,7 +54,7 @@ object APIHolder {
) )
val restrictedApis = arrayListOf( val restrictedApis = arrayListOf(
TrailersToProvider(), // be aware that this is fuckery // TrailersToProvider(), // be aware that this is fuckery
// NyaaProvider(), // torrents in cs3 is wack // NyaaProvider(), // torrents in cs3 is wack
// ThenosProvider(), // ddos protection and wacked links // ThenosProvider(), // ddos protection and wacked links
AsiaFlixProvider(), AsiaFlixProvider(),
@ -314,7 +314,7 @@ interface SearchResponse {
val name: String val name: String
val url: String val url: String
val apiName: String val apiName: String
val type: TvType val type: TvType?
val posterUrl: String? val posterUrl: String?
val id: Int? 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.mvvm.logError
import com.lagradost.cloudstream3.network.initRequestClient import com.lagradost.cloudstream3.network.initRequestClient
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.OAuth2accountApis import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.OAuth2accountApis
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.appString import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.appString
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.player.PlayerEventType 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.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission 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.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
@ -447,6 +448,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view.setupWithNavController(navController) nav_view.setupWithNavController(navController)
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
this.hideKeyboard()
// nav_view.hideKeyboard() // nav_view.hideKeyboard()
/*if (destination.id != R.id.navigation_player) { /*if (destination.id != R.id.navigation_player) {
requestedOrientation = if (settingsManager?.getBoolean("force_landscape", false) == true) { 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 android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty 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.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.post import com.lagradost.cloudstream3.network.post
import com.lagradost.cloudstream3.network.text import com.lagradost.cloudstream3.network.text
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.appString import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.maxStale import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.unixTime 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.openBrowser
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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 com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL import java.net.URL
import java.util.* 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 override val name: String
get() = "AniList" get() = "AniList"
override val key: String override val key: String
@ -32,11 +35,19 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
get() = "anilistlogin" get() = "anilistlogin"
override val idPrefix: String override val idPrefix: String
get() = "anilist" 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.getUser(true)?.
context.getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user -> 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 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 { 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") private val aniListStatusString = arrayOf("CURRENT", "COMPLETED", "PAUSED", "DROPPED", "PLANNING", "REPEATING")
const val ANILIST_UNIXTIME_KEY: String = "anilist_unixtime" // When token expires const val ANILIST_UNIXTIME_KEY: String = "anilist_unixtime" // When token expires
@ -79,89 +143,14 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
const val ANILIST_CACHED_LIST: String = "anilist_cached_list" const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list" const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list"
}
private fun fixName(name: String): String {
private val mapper = JsonMapper.builder().addModule(KotlinModule()) return name.toLowerCase(Locale.ROOT).replace(" ", "").replace("[^a-zA-Z0-9]".toRegex(), "")
.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 { private fun searchShows(name: String): GetSearchRoot? {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string)) try {
} val query = """
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(), "")
}
private fun searchShows(name: String): GetSearchRoot? {
try {
val query = """
query (${"$"}id: Int, ${"$"}page: Int, ${"$"}search: String, ${"$"}type: MediaType) { query (${"$"}id: Int, ${"$"}page: Int, ${"$"}search: String, ${"$"}type: MediaType) {
Page (page: ${"$"}page, perPage: 10) { Page (page: ${"$"}page, perPage: 10) {
media (id: ${"$"}id, search: ${"$"}search, type: ${"$"}type) { media (id: ${"$"}id, search: ${"$"}search, type: ${"$"}type) {
@ -216,91 +205,180 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
} }
} }
""" """
val data = val data =
mapOf("query" to query, "variables" to mapper.writeValueAsString(mapOf("search" to name, "page" to 1, "type" to "ANIME")) ) mapOf(
"query" to query,
val res = post( "variables" to mapper.writeValueAsString(
"https://graphql.anilist.co/", mapOf(
//headers = mapOf(), "search" to name,
data = data,//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) "page" to 1,
timeout = 5000 // REASONABLE TIMEOUT "type" to "ANIME"
).text.replace("\\", "") )
return res.toKotlinObject()
} catch (e: Exception) {
logError(e)
}
return null
}
// Should use https://gist.github.com/purplepinapples/5dc60f15f2837bf1cea71b089cfeaa0a
fun getShowId(malId: String?, name: String, year: Int?): GetSearchMedia? {
// Strips these from the name
val blackList = listOf(
"TV Dubbed",
"(Dub)",
"Subbed",
"(TV)",
"(Uncensored)",
"(Censored)",
"(\\d+)" // year
)
val blackListRegex =
Regex(""" (${blackList.joinToString(separator = "|").replace("(", "\\(").replace(")", "\\)")})""")
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
val shows = searchShows(name.replace(blackListRegex, ""))
shows?.data?.Page?.media?.find {
malId ?: "NONE" == it.idMal.toString()
}?.let { return it }
val filtered =
shows?.data?.Page?.media?.filter {
(
it.startDate.year ?: year.toString() == year.toString()
|| year == null
) )
} )
filtered?.forEach {
if (fixName(it.title.romaji) == fixName(name)) return it
}
return filtered?.firstOrNull() val res = post(
}
private fun Context.postApi(url: String, q: String, cache: Boolean = false): String {
return try {
if (!checkToken()) {
// println("VARS_ " + vars)
post(
"https://graphql.anilist.co/", "https://graphql.anilist.co/",
headers = mapOf( //headers = mapOf(),
"Authorization" to "Bearer " + getKey( data = data,//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
accountId, timeout = 5000 // REASONABLE TIMEOUT
ANILIST_TOKEN_KEY, ).text.replace("\\", "")
"" return res.toKotlinObject()
)!!, } catch (e: Exception) {
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" logError(e)
), }
cacheTime = 0, return null
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("\\/", "/") // Should use https://gist.github.com/purplepinapples/5dc60f15f2837bf1cea71b089cfeaa0a
} else { fun getShowId(malId: String?, name: String, year: Int?): GetSearchMedia? {
"" // Strips these from the name
val blackList = listOf(
"TV Dubbed",
"(Dub)",
"Subbed",
"(TV)",
"(Uncensored)",
"(Censored)",
"(\\d+)" // year
)
val blackListRegex =
Regex(""" (${blackList.joinToString(separator = "|").replace("(", "\\(").replace(")", "\\)")})""")
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
val shows = searchShows(name.replace(blackListRegex, ""))
shows?.data?.Page?.media?.find {
malId ?: "NONE" == it.idMal.toString()
}?.let { return it }
val filtered =
shows?.data?.Page?.media?.filter {
(
it.startDate.year ?: year.toString() == year.toString()
|| year == null
)
}
filtered?.forEach {
if (fixName(it.title.romaji) == fixName(name)) return it
}
return filtered?.firstOrNull()
}
// 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))
}
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
} }
} catch (e: Exception) {
logError(e)
""
} }
} }
data class MediaRecommendation( fun Context.initGetUser() {
@JsonProperty("id") val id: Int, if (getKey<String>(accountId, ANILIST_TOKEN_KEY, null) == null) return
@JsonProperty("title") val title: Title, ioSafe {
@JsonProperty("idMal") val idMal: Int?, getUser()
@JsonProperty("coverImage") val coverImage: CoverImage, }
@JsonProperty("averageScore") val averageScore: Int? }
)
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? { fun Context.getDataAboutId(id: Int): AniListTitleHolder? {
val q = 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( data class FullAnilistList(
@JsonProperty("data") val data: Data @JsonProperty("data") val data: Data
) )
@ -530,7 +643,7 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
return data != "" 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 { try {
val q = val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ """mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
@ -538,7 +651,7 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
0, 0,
type.value 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) { SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
id id
status 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?> { fun getAllSeasons(id: Int): List<SeasonResponse?> {
val seasons = mutableListOf<SeasonResponse?>() val seasons = mutableListOf<SeasonResponse?>()
fun getSeasonRecursive(id: Int) { fun getSeasonRecursive(id: Int) {
@ -662,27 +732,6 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
return seasons.toList() 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( data class SeasonResponse(
@JsonProperty("data") val data: SeasonData, @JsonProperty("data") val data: SeasonData,
) )
@ -725,9 +774,9 @@ class AniListApi(index : Int) : OAuth2Interface.AccountManager(index) {
data class SeasonNode( data class SeasonNode(
@JsonProperty("id") val id: Int, @JsonProperty("id") val id: Int,
@JsonProperty("format") val format: String?, @JsonProperty("format") val format: String?,
@JsonProperty("title") val title: AniListApi.Title, @JsonProperty("title") val title: Title,
@JsonProperty("idMal") val idMal: Int?, @JsonProperty("idMal") val idMal: Int?,
@JsonProperty("coverImage") val coverImage: AniListApi.CoverImage, @JsonProperty("coverImage") val coverImage: CoverImage,
@JsonProperty("averageScore") val averageScore: Int? @JsonProperty("averageScore") val averageScore: Int?
// @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?, // @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 android.content.Context
import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync //TODO dropbox sync
class Dropbox : OAuth2Interface { class Dropbox : OAuth2API {
override val name: String override val name: String
get() = "Dropbox" get() = "Dropbox"
override val key: String override val key: String
@ -23,7 +24,7 @@ class Dropbox : OAuth2Interface {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? { override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
TODO("Not yet implemented") 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.content.Context
import android.util.Base64 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.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.get import com.lagradost.cloudstream3.network.get
import com.lagradost.cloudstream3.network.post import com.lagradost.cloudstream3.network.post
import com.lagradost.cloudstream3.network.put import com.lagradost.cloudstream3.network.put
import com.lagradost.cloudstream3.network.text import com.lagradost.cloudstream3.network.text
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.appString import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.secondsToReadable import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.unixTime 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.openBrowser
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -27,7 +31,10 @@ import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* 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 override val name: String
get() = "MAL" get() = "MAL"
override val key: String override val key: String
@ -36,19 +43,70 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
get() = "mallogin" get() = "mallogin"
override val idPrefix: String override val idPrefix: String
get() = "mal" get() = "mal"
override val mainUrl: String
get() = "https://myanimelist.net"
override val icon: Int
get() = R.drawable.mal_logo
override fun logOut(context: Context) { override fun logOut(context: Context) {
context.removeAccountKeys() context.removeAccountKeys()
} }
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? { override fun loginInfo(context: Context): OAuth2API.LoginInfo? {
//context.getMalUser(true)? //context.getMalUser(true)?
context.getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user -> 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 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 { 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")
@ -60,7 +118,7 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
} }
override fun handleRedirect(context: Context,url: String) { override fun handleRedirect(context: Context, url: String) {
try { try {
val sanitizer = val sanitizer =
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
@ -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 { 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 = "https://api.myanimelist.net/v2/anime/$id?fields=id,title,num_episodes,my_list_status" 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) 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) { return when (inp) {
-1 -> MalStatusType.None -1 -> MalStatusType.None
0 -> MalStatusType.Watching 0 -> MalStatusType.Watching
@ -534,12 +592,23 @@ class MALApi(index : Int) : OAuth2Interface.AccountManager(index) {
@JsonProperty("picture") val picture: String, @JsonProperty("picture") val picture: String,
) )
data class MalMainPicture(
@JsonProperty("large") val large: String?,
@JsonProperty("medium") val medium: String?,
)
// Used for getDataAboutId() // Used for getDataAboutId()
data class MalAnime( data class MalAnime(
@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,
@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( 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.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe 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.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.AutofitRecyclerView
@ -430,7 +430,7 @@ class HomeFragment : Fragment() {
// nice profile pic on homepage // nice profile pic on homepage
home_profile_picture_holder?.isVisible = false home_profile_picture_holder?.isVisible = false
context?.let { ctx -> context?.let { ctx ->
for (syncApi in OAuth2Interface.OAuth2Apis) { for (syncApi in OAuth2API.OAuth2Apis) {
val login = syncApi.loginInfo(ctx) val login = syncApi.loginInfo(ctx)
val pic = login?.profilePicture val pic = login?.profilePicture
if (pic != null) { if (pic != null) {

View file

@ -8,13 +8,13 @@ import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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( class AccountAdapter(
val cardList: List<OAuth2Interface.LoginInfo>, val cardList: List<OAuth2API.LoginInfo>,
val layout: Int = R.layout.account_single, val layout: Int = R.layout.account_single,
private val clickCallback: (AccountClickCallback) -> Unit private val clickCallback: (AccountClickCallback) -> Unit
) : ) :
@ -48,7 +48,7 @@ class AccountAdapter(
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
private val accountName: TextView = itemView.findViewById(R.id.account_name)!! 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 // 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) accountName.text = card.name ?: "%s %d".format(accountName.context.getString(R.string.account), card.accountIndex)
if(card.profilePicture.isNullOrEmpty()) { 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.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initRequestClient import com.lagradost.cloudstream3.network.initRequestClient
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.Companion.malApi 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.APIRepository
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
@ -120,7 +121,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
Triple("🇹🇷", "Turkish", "tr") Triple("🇹🇷", "Turkish", "tr")
).sortedBy { it.second } //ye, we go alphabetical, so ppl don't put their lang on top ).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 = val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch) AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_switch)
val dialog = builder.show() val dialog = builder.show()
@ -132,7 +133,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
val ogIndex = api.accountIndex val ogIndex = api.accountIndex
val items = ArrayList<OAuth2Interface.LoginInfo>() val items = ArrayList<OAuth2API.LoginInfo>()
for (index in accounts) { for (index in accounts) {
api.accountIndex = index api.accountIndex = index
@ -150,7 +151,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
list?.adapter = adapter 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 = val builder =
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_managment) AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_managment)
val dialog = builder.show() 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,10 +67,15 @@ object UIHelper {
fun Fragment.hideKeyboard() { fun Fragment.hideKeyboard() {
activity?.window?.decorView?.clearFocus() activity?.window?.decorView?.clearFocus()
view.let { view?.let {
if (it != null) { hideKeyboard(it)
hideKeyboard(it) }
} }
fun Activity.hideKeyboard() {
window?.decorView?.clearFocus()
this.findViewById<View>(android.R.id.content)?.rootView?.let {
hideKeyboard(it)
} }
} }

View file

@ -84,6 +84,7 @@
<string name="type_dropped">Dropped</string> <string name="type_dropped">Dropped</string>
<string name="type_plan_to_watch">Plan to Watch</string> <string name="type_plan_to_watch">Plan to Watch</string>
<string name="type_none">None</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_movie_button">Play Movie</string>
<string name="play_torrent_button">Stream Torrent</string> <string name="play_torrent_button">Stream Torrent</string>

View file

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