forked from recloudstream/cloudstream
anilist/mal api, (NO UI YET)
This commit is contained in:
parent
d323092f11
commit
d6870836d9
17 changed files with 1845 additions and 48 deletions
|
@ -63,6 +63,12 @@
|
||||||
<data android:scheme="https" android:host="vidembed.cc" android:pathPrefix="/"/>
|
<data android:scheme="https" android:host="vidembed.cc" android:pathPrefix="/"/>
|
||||||
<data android:scheme="https" android:host="trailers.to" android:pathPrefix="/"/>
|
<data android:scheme="https" android:host="trailers.to" android:pathPrefix="/"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT"/>
|
||||||
|
<category android:name="android.intent.category.BROWSABLE"/>
|
||||||
|
<data android:scheme="cloudstreamapp"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver
|
<receiver
|
||||||
|
|
|
@ -28,6 +28,8 @@ 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.OAuth2Interface.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
|
||||||
|
@ -347,6 +349,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
val str = intent.dataString
|
val str = intent.dataString
|
||||||
if (str != null) {
|
if (str != null) {
|
||||||
|
if (str.contains(appString)) {
|
||||||
|
for (api in OAuth2Apis) {
|
||||||
|
if (str.contains("/${api.redirectUrl}")) {
|
||||||
|
api.handleRedirect(this, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
||||||
this.navigate(R.id.navigation_downloads)
|
this.navigate(R.id.navigation_downloads)
|
||||||
} else {
|
} else {
|
||||||
|
@ -359,6 +368,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
|
@ -65,10 +65,12 @@ val Response.cookies: Map<String, String>
|
||||||
}?.filter { it.key.isNotBlank() && it.value.isNotBlank() } ?: mapOf()
|
}?.filter { it.key.isNotBlank() && it.value.isNotBlank() } ?: mapOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getData(data: Map<String, String>): RequestBody {
|
fun getData(data: Map<String, String?>): RequestBody {
|
||||||
val builder = FormBody.Builder()
|
val builder = FormBody.Builder()
|
||||||
data.forEach {
|
data.forEach {
|
||||||
builder.add(it.key, it.value)
|
it.value?.let { value ->
|
||||||
|
builder.add(it.key, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return builder.build()
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
@ -83,10 +85,12 @@ fun appendUri(uri: String, appendQuery: String): String {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can probably be done recursively
|
// Can probably be done recursively
|
||||||
fun addParamsToUrl(url: String, params: Map<String, String>): String {
|
fun addParamsToUrl(url: String, params: Map<String, String?>): String {
|
||||||
var appendedUrl = url
|
var appendedUrl = url
|
||||||
params.forEach {
|
params.forEach {
|
||||||
appendedUrl = appendUri(appendedUrl, "${it.key}=${it.value}")
|
it.value?.let { value ->
|
||||||
|
appendedUrl = appendUri(appendedUrl, "${it.key}=${value}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return appendedUrl
|
return appendedUrl
|
||||||
}
|
}
|
||||||
|
@ -189,3 +193,44 @@ fun postRequestCreator(
|
||||||
.post(getData(data))
|
.post(getData(data))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun putRequestCreator(
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
referer: String?,
|
||||||
|
params: Map<String, String?>,
|
||||||
|
cookies: Map<String, String>,
|
||||||
|
data: Map<String, String?>,
|
||||||
|
cacheTime: Int,
|
||||||
|
cacheUnit: TimeUnit
|
||||||
|
): Request {
|
||||||
|
return Request.Builder()
|
||||||
|
.url(addParamsToUrl(url, params))
|
||||||
|
.cacheControl(getCache(cacheTime, cacheUnit))
|
||||||
|
.headers(getHeaders(headers, referer, cookies))
|
||||||
|
.put(getData(data))
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun put(
|
||||||
|
url: String,
|
||||||
|
headers: Map<String, String> = mapOf(),
|
||||||
|
referer: String? = null,
|
||||||
|
params: Map<String, String> = mapOf(),
|
||||||
|
cookies: Map<String, String> = mapOf(),
|
||||||
|
data: Map<String, String?> = DEFAULT_DATA,
|
||||||
|
allowRedirects: Boolean = true,
|
||||||
|
cacheTime: Int = DEFAULT_TIME,
|
||||||
|
cacheUnit: TimeUnit = DEFAULT_TIME_UNIT,
|
||||||
|
timeout: Long = 0L
|
||||||
|
): Response {
|
||||||
|
val client = baseClient
|
||||||
|
.newBuilder()
|
||||||
|
.followRedirects(allowRedirects)
|
||||||
|
.followSslRedirects(allowRedirects)
|
||||||
|
.callTimeout(timeout, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
val request = putRequestCreator(url, headers, referer, params, cookies, data, cacheTime, cacheUnit)
|
||||||
|
return client.newCall(request).execute()
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,874 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
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.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.utils.AppUtils.openBrowser
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
|
||||||
|
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(var accountId: String) : OAuth2Interface {
|
||||||
|
override val name: String
|
||||||
|
get() = "AniList"
|
||||||
|
override val key: String
|
||||||
|
get() = "6871"
|
||||||
|
override val redirectUrl: String
|
||||||
|
get() = "anilistlogin"
|
||||||
|
|
||||||
|
override fun logOut(context: Context) {
|
||||||
|
context.removeKeys(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? {
|
||||||
|
// context.getUser(true)?.
|
||||||
|
context.getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.let { user ->
|
||||||
|
return OAuth2Interface.LoginInfo(profilePicture = user.picture, name = user.name)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun authenticate(context: Context) {
|
||||||
|
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
|
||||||
|
context.openBrowser(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRedirect(context: Context, url: String) {
|
||||||
|
try {
|
||||||
|
val sanitizer =
|
||||||
|
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
|
val token = sanitizer["access_token"]!!
|
||||||
|
val expiresIn = sanitizer["expires_in"]!!
|
||||||
|
|
||||||
|
val endTime = unixTime + expiresIn.toLong()
|
||||||
|
|
||||||
|
context.setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
|
||||||
|
context.setKey(accountId, ANILIST_TOKEN_KEY, token)
|
||||||
|
context.setKey(ANILIST_SHOULD_UPDATE_LIST, true)
|
||||||
|
ioSafe {
|
||||||
|
context.getUser()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val aniListStatusString = arrayOf("CURRENT", "COMPLETED", "PAUSED", "DROPPED", "PLANNING", "REPEATING")
|
||||||
|
|
||||||
|
const val ANILIST_UNIXTIME_KEY: String = "anilist_unixtime" // When token expires
|
||||||
|
const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api
|
||||||
|
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(), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchShows(name: String): GetSearchRoot? {
|
||||||
|
try {
|
||||||
|
val query = """
|
||||||
|
query (${"$"}id: Int, ${"$"}page: Int, ${"$"}search: String, ${"$"}type: MediaType) {
|
||||||
|
Page (page: ${"$"}page, perPage: 10) {
|
||||||
|
media (id: ${"$"}id, search: ${"$"}search, type: ${"$"}type) {
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
seasonYear
|
||||||
|
startDate { year month day }
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
}
|
||||||
|
averageScore
|
||||||
|
meanScore
|
||||||
|
nextAiringEpisode {
|
||||||
|
timeUntilAiring
|
||||||
|
episode
|
||||||
|
}
|
||||||
|
trailer { id site thumbnail }
|
||||||
|
bannerImage
|
||||||
|
recommendations {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
mediaRecommendation {
|
||||||
|
id
|
||||||
|
title {
|
||||||
|
english
|
||||||
|
romaji
|
||||||
|
}
|
||||||
|
idMal
|
||||||
|
coverImage { medium large }
|
||||||
|
averageScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relations {
|
||||||
|
edges {
|
||||||
|
id
|
||||||
|
relationType(version: 2)
|
||||||
|
node {
|
||||||
|
format
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
coverImage { medium large }
|
||||||
|
averageScore
|
||||||
|
title {
|
||||||
|
english
|
||||||
|
romaji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
val data =
|
||||||
|
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/",
|
||||||
|
//headers = mapOf(),
|
||||||
|
data = data,//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
|
||||||
|
timeout = 5000 // REASONABLE TIMEOUT
|
||||||
|
).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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.getDataAboutId(id: Int): AniListTitleHolder? {
|
||||||
|
val q =
|
||||||
|
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
|
||||||
|
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
|
||||||
|
id
|
||||||
|
episodes
|
||||||
|
isFavourite
|
||||||
|
mediaListEntry {
|
||||||
|
progress
|
||||||
|
status
|
||||||
|
score (format: POINT_10)
|
||||||
|
}
|
||||||
|
title {
|
||||||
|
english
|
||||||
|
romaji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
try {
|
||||||
|
val data = postApi("https://graphql.anilist.co", q, true)
|
||||||
|
var d: GetDataRoot? = null
|
||||||
|
try {
|
||||||
|
d = mapper.readValue<GetDataRoot>(data)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
println("AniList json failed")
|
||||||
|
}
|
||||||
|
if (d == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val main = d.data.Media
|
||||||
|
if (main.mediaListEntry != null) {
|
||||||
|
return AniListTitleHolder(
|
||||||
|
title = main.title,
|
||||||
|
id = id,
|
||||||
|
isFavourite = main.isFavourite,
|
||||||
|
progress = main.mediaListEntry.progress,
|
||||||
|
episodes = main.episodes,
|
||||||
|
score = main.mediaListEntry.score,
|
||||||
|
type = fromIntToAnimeStatus(aniListStatusString.indexOf(main.mediaListEntry.status)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return AniListTitleHolder(
|
||||||
|
title = main.title,
|
||||||
|
id = id,
|
||||||
|
isFavourite = main.isFavourite,
|
||||||
|
progress = 0,
|
||||||
|
episodes = main.episodes,
|
||||||
|
score = 0,
|
||||||
|
type = AniListStatusType.None,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class FullAnilistList(
|
||||||
|
@JsonProperty("data") val data: Data
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CompletedAt(
|
||||||
|
@JsonProperty("year") val year: Int,
|
||||||
|
@JsonProperty("month") val month: Int,
|
||||||
|
@JsonProperty("day") val day: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StartedAt(
|
||||||
|
@JsonProperty("year") val year: String?,
|
||||||
|
@JsonProperty("month") val month: String?,
|
||||||
|
@JsonProperty("day") val day: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Title(
|
||||||
|
@JsonProperty("english") val english: String?,
|
||||||
|
@JsonProperty("romaji") val romaji: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CoverImage(
|
||||||
|
@JsonProperty("medium") val medium: String,
|
||||||
|
@JsonProperty("large") val large: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Media(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("idMal") val idMal: Int?,
|
||||||
|
@JsonProperty("season") val season: String?,
|
||||||
|
@JsonProperty("seasonYear") val seasonYear: Int,
|
||||||
|
@JsonProperty("format") val format: String?,
|
||||||
|
//@JsonProperty("source") val source: String,
|
||||||
|
@JsonProperty("episodes") val episodes: Int,
|
||||||
|
@JsonProperty("title") val title: Title,
|
||||||
|
//@JsonProperty("description") val description: String,
|
||||||
|
@JsonProperty("coverImage") val coverImage: CoverImage,
|
||||||
|
@JsonProperty("synonyms") val synonyms: List<String>,
|
||||||
|
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Entries(
|
||||||
|
@JsonProperty("status") val status: String?,
|
||||||
|
@JsonProperty("completedAt") val completedAt: CompletedAt,
|
||||||
|
@JsonProperty("startedAt") val startedAt: StartedAt,
|
||||||
|
@JsonProperty("updatedAt") val updatedAt: Int,
|
||||||
|
@JsonProperty("progress") val progress: Int,
|
||||||
|
@JsonProperty("score") val score: Int,
|
||||||
|
@JsonProperty("private") val private: Boolean,
|
||||||
|
@JsonProperty("media") val media: Media
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Lists(
|
||||||
|
@JsonProperty("status") val status: String?,
|
||||||
|
@JsonProperty("entries") val entries: List<Entries>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MediaListCollection(
|
||||||
|
@JsonProperty("lists") val lists: List<Lists>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.getAnilistListCached(): Array<Lists>? {
|
||||||
|
return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getAnilistAnimeListSmart(): Array<Lists>? {
|
||||||
|
if (getKey<String>(
|
||||||
|
accountId,
|
||||||
|
ANILIST_TOKEN_KEY,
|
||||||
|
null
|
||||||
|
) == null
|
||||||
|
) return null
|
||||||
|
|
||||||
|
if (checkToken()) return null
|
||||||
|
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) {
|
||||||
|
val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray()
|
||||||
|
if (list != null) {
|
||||||
|
setKey(ANILIST_CACHED_LIST, list)
|
||||||
|
setKey(ANILIST_SHOULD_UPDATE_LIST, false)
|
||||||
|
}
|
||||||
|
list
|
||||||
|
} else {
|
||||||
|
getAnilistListCached()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.getFullAnilistList(): FullAnilistList? {
|
||||||
|
try {
|
||||||
|
var userID: Int? = null
|
||||||
|
/** WARNING ASSUMES ONE USER! **/
|
||||||
|
getKeys(ANILIST_USER_KEY).forEach { key ->
|
||||||
|
getKey<AniListUser>(key, null)?.let {
|
||||||
|
userID = it.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fixedUserID = userID ?: return null
|
||||||
|
val mediaType = "ANIME"
|
||||||
|
|
||||||
|
val query = """
|
||||||
|
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) {
|
||||||
|
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
|
||||||
|
lists {
|
||||||
|
status
|
||||||
|
entries
|
||||||
|
{
|
||||||
|
status
|
||||||
|
completedAt { year month day }
|
||||||
|
startedAt { year month day }
|
||||||
|
updatedAt
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
private
|
||||||
|
media
|
||||||
|
{
|
||||||
|
id
|
||||||
|
idMal
|
||||||
|
season
|
||||||
|
seasonYear
|
||||||
|
format
|
||||||
|
episodes
|
||||||
|
chapters
|
||||||
|
title
|
||||||
|
{
|
||||||
|
english
|
||||||
|
romaji
|
||||||
|
}
|
||||||
|
coverImage { medium }
|
||||||
|
synonyms
|
||||||
|
nextAiringEpisode {
|
||||||
|
timeUntilAiring
|
||||||
|
episode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
val text = postApi("https://graphql.anilist.co", query)
|
||||||
|
return text.toKotlinObject()
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.toggleLike(id: Int): Boolean {
|
||||||
|
val q = """mutation (${'$'}animeId: Int = $id) {
|
||||||
|
ToggleFavourite (animeId: ${'$'}animeId) {
|
||||||
|
anime {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
title {
|
||||||
|
romaji
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
val data = postApi("https://graphql.anilist.co", q)
|
||||||
|
return data != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.postDataAboutId(id: Int, type: AniListStatusType, score: Int, progress: Int): Boolean {
|
||||||
|
try {
|
||||||
|
val q =
|
||||||
|
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
|
||||||
|
aniListStatusString[maxOf(
|
||||||
|
0,
|
||||||
|
type.value
|
||||||
|
)]
|
||||||
|
}, ${'$'}scoreRaw: Int = ${score * 10}, ${'$'}progress: Int = $progress) {
|
||||||
|
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
progress
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
val data = postApi("https://graphql.anilist.co", q)
|
||||||
|
return data != ""
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getUser(setSettings: Boolean = true): AniListUser? {
|
||||||
|
val q = """
|
||||||
|
{
|
||||||
|
Viewer {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
avatar {
|
||||||
|
large
|
||||||
|
}
|
||||||
|
favourites {
|
||||||
|
anime {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"""
|
||||||
|
try {
|
||||||
|
val data = postApi("https://graphql.anilist.co", q)
|
||||||
|
if (data == "") return null
|
||||||
|
val userData = mapper.readValue<AniListRoot>(data)
|
||||||
|
val u = userData.data.Viewer
|
||||||
|
val user = AniListUser(
|
||||||
|
u.id,
|
||||||
|
u.name,
|
||||||
|
u.avatar.large,
|
||||||
|
)
|
||||||
|
if (setSettings) {
|
||||||
|
setKey(accountId, ANILIST_USER_KEY, user)
|
||||||
|
}
|
||||||
|
/* // 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
val season = getSeason(id)
|
||||||
|
if (season != null) {
|
||||||
|
seasons.add(season)
|
||||||
|
if (season.data.Media.format?.startsWith("TV") == true) {
|
||||||
|
season.data.Media.relations.edges.forEach {
|
||||||
|
if (it.node.format != null) {
|
||||||
|
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
|
||||||
|
getSeasonRecursive(it.node.id)
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getSeasonRecursive(id)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeasonData(
|
||||||
|
@JsonProperty("Media") val Media: SeasonMedia,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeasonMedia(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("idMal") val idMal: Int?,
|
||||||
|
@JsonProperty("format") val format: String?,
|
||||||
|
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
|
||||||
|
@JsonProperty("relations") val relations: SeasonEdges,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeasonNextAiringEpisode(
|
||||||
|
@JsonProperty("episode") val episode: Int,
|
||||||
|
@JsonProperty("timeUntilAiring") val timeUntilAiring: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeasonEdges(
|
||||||
|
@JsonProperty("edges") val edges: List<SeasonEdge>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeasonEdge(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("relationType") val relationType: String,
|
||||||
|
@JsonProperty("node") val node: SeasonNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListFavoritesMediaConnection(
|
||||||
|
@JsonProperty("nodes") val nodes: List<LikeNode>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListFavourites(
|
||||||
|
@JsonProperty("anime") val anime: AniListFavoritesMediaConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SeasonNode(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("format") val format: String?,
|
||||||
|
@JsonProperty("title") val title: AniListApi.Title,
|
||||||
|
@JsonProperty("idMal") val idMal: Int?,
|
||||||
|
@JsonProperty("coverImage") val coverImage: AniListApi.CoverImage,
|
||||||
|
@JsonProperty("averageScore") val averageScore: Int?
|
||||||
|
// @JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListAvatar(
|
||||||
|
@JsonProperty("large") val large: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListViewer(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("avatar") val avatar: AniListAvatar,
|
||||||
|
@JsonProperty("favourites") val favourites: AniListFavourites,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListData(
|
||||||
|
@JsonProperty("Viewer") val Viewer: AniListViewer,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListRoot(
|
||||||
|
@JsonProperty("data") val data: AniListData,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListUser(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("picture") val picture: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikeNode(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
//@JsonProperty("idMal") public int idMal;
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikePageInfo(
|
||||||
|
@JsonProperty("total") val total: Int,
|
||||||
|
@JsonProperty("currentPage") val currentPage: Int,
|
||||||
|
@JsonProperty("lastPage") val lastPage: Int,
|
||||||
|
@JsonProperty("perPage") val perPage: Int,
|
||||||
|
@JsonProperty("hasNextPage") val hasNextPage: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikeAnime(
|
||||||
|
@JsonProperty("nodes") val nodes: List<LikeNode>,
|
||||||
|
@JsonProperty("pageInfo") val pageInfo: LikePageInfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikeFavourites(
|
||||||
|
@JsonProperty("anime") val anime: LikeAnime,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikeViewer(
|
||||||
|
@JsonProperty("favourites") val favourites: LikeFavourites,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikeData(
|
||||||
|
@JsonProperty("Viewer") val Viewer: LikeViewer,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LikeRoot(
|
||||||
|
@JsonProperty("data") val data: LikeData,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Recommendation(
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("idMal") val idMal: Int,
|
||||||
|
@JsonProperty("poster") val poster: String,
|
||||||
|
@JsonProperty("averageScore") val averageScore: Int?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AniListTitleHolder(
|
||||||
|
@JsonProperty("title") val title: Title,
|
||||||
|
@JsonProperty("isFavourite") val isFavourite: Boolean,
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("progress") val progress: Int,
|
||||||
|
@JsonProperty("episodes") val episodes: Int,
|
||||||
|
@JsonProperty("score") val score: Int,
|
||||||
|
@JsonProperty("type") val type: AniListStatusType,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetDataMediaListEntry(
|
||||||
|
@JsonProperty("progress") val progress: Int,
|
||||||
|
@JsonProperty("status") val status: String,
|
||||||
|
@JsonProperty("score") val score: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Nodes(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("mediaRecommendation") val mediaRecommendation: MediaRecommendation?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetDataMedia(
|
||||||
|
@JsonProperty("isFavourite") val isFavourite: Boolean,
|
||||||
|
@JsonProperty("episodes") val episodes: Int,
|
||||||
|
@JsonProperty("title") val title: Title,
|
||||||
|
@JsonProperty("mediaListEntry") val mediaListEntry: GetDataMediaListEntry?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Recommendations(
|
||||||
|
@JsonProperty("nodes") val nodes: List<Nodes>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetDataData(
|
||||||
|
@JsonProperty("Media") val Media: GetDataMedia,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetDataRoot(
|
||||||
|
@JsonProperty("data") val data: GetDataData,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetSearchTitle(
|
||||||
|
@JsonProperty("romaji") val romaji: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TrailerObject(
|
||||||
|
@JsonProperty("id") val id: String?,
|
||||||
|
@JsonProperty("thumbnail") val thumbnail: String?,
|
||||||
|
@JsonProperty("site") val site: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetSearchMedia(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("idMal") val idMal: Int?,
|
||||||
|
@JsonProperty("seasonYear") val seasonYear: Int,
|
||||||
|
@JsonProperty("title") val title: GetSearchTitle,
|
||||||
|
@JsonProperty("startDate") val startDate: StartedAt,
|
||||||
|
@JsonProperty("averageScore") val averageScore: Int?,
|
||||||
|
@JsonProperty("meanScore") val meanScore: Int?,
|
||||||
|
@JsonProperty("bannerImage") val bannerImage: String?,
|
||||||
|
@JsonProperty("trailer") val trailer: TrailerObject?,
|
||||||
|
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
|
||||||
|
@JsonProperty("recommendations") val recommendations: Recommendations?,
|
||||||
|
@JsonProperty("relations") val relations: SeasonEdges
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetSearchPage(
|
||||||
|
@JsonProperty("Page") val Page: GetSearchData,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetSearchData(
|
||||||
|
@JsonProperty("media") val media: List<GetSearchMedia>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetSearchRoot(
|
||||||
|
@JsonProperty("data") val data: GetSearchPage,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +1,29 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
//TODO dropbox sync
|
//TODO dropbox sync
|
||||||
class Dropbox : OAuth2Interface {
|
class Dropbox : OAuth2Interface {
|
||||||
|
override val name: String
|
||||||
|
get() = "Dropbox"
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = "zlqsamadlwydvb2"
|
get() = "zlqsamadlwydvb2"
|
||||||
override val redirectUrl: String
|
override val redirectUrl: String
|
||||||
get() = "dropboxlogin"
|
get() = "dropboxlogin"
|
||||||
|
|
||||||
override fun handleRedirect(url: String) {
|
override fun authenticate(context: Context) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRedirect(context: Context,url: String) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun logOut(context: Context) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? {
|
||||||
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,547 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
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.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.utils.AppUtils.openBrowser
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
|
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||||
|
import java.net.URL
|
||||||
|
import java.security.SecureRandom
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class MALApi(var accountId: String) : OAuth2Interface {
|
||||||
|
override val name: String
|
||||||
|
get() = "MAL"
|
||||||
|
override val key: String
|
||||||
|
get() = "1714d6f2f4f7cc19644384f8c4629910"
|
||||||
|
override val redirectUrl: String
|
||||||
|
get() = "mallogin"
|
||||||
|
|
||||||
|
override fun logOut(context: Context) {
|
||||||
|
context.removeKeys(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loginInfo(context: Context): OAuth2Interface.LoginInfo? {
|
||||||
|
//context.getMalUser(true)?
|
||||||
|
context.getKey<MalUser>(accountId, MAL_USER_KEY)?.let { user ->
|
||||||
|
return OAuth2Interface.LoginInfo(profilePicture = user.picture, name = user.name)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val malStatusAsString = arrayOf("watching", "completed", "on_hold", "dropped", "plan_to_watch")
|
||||||
|
|
||||||
|
const val MAL_USER_KEY: String = "mal_user" // user data like profile
|
||||||
|
const val MAL_CACHED_LIST: String = "mal_cached_list"
|
||||||
|
const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list"
|
||||||
|
const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires
|
||||||
|
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
|
||||||
|
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRedirect(context: Context,url: String) {
|
||||||
|
try {
|
||||||
|
val sanitizer =
|
||||||
|
splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
|
||||||
|
val state = sanitizer["state"]!!
|
||||||
|
if (state == "RequestID$requestId") {
|
||||||
|
val currentCode = sanitizer["code"]!!
|
||||||
|
ioSafe {
|
||||||
|
var res = ""
|
||||||
|
try {
|
||||||
|
//println("cc::::: " + codeVerifier)
|
||||||
|
res = post(
|
||||||
|
"https://myanimelist.net/v1/oauth2/token",
|
||||||
|
data = mapOf(
|
||||||
|
"client_id" to key,
|
||||||
|
"code" to currentCode,
|
||||||
|
"code_verifier" to codeVerifier,
|
||||||
|
"grant_type" to "authorization_code"
|
||||||
|
)
|
||||||
|
).text
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res != "") {
|
||||||
|
context.storeToken(res)
|
||||||
|
context.getMalUser()
|
||||||
|
context.setKey(MAL_SHOULD_UPDATE_LIST, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun authenticate(context: Context) {
|
||||||
|
// It is recommended to use a URL-safe string as code_verifier.
|
||||||
|
// See section 4 of RFC 7636 for more details.
|
||||||
|
|
||||||
|
val secureRandom = SecureRandom()
|
||||||
|
val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128
|
||||||
|
secureRandom.nextBytes(codeVerifierBytes)
|
||||||
|
codeVerifier =
|
||||||
|
Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=').replace("+", "-")
|
||||||
|
.replace("/", "_").replace("\n", "")
|
||||||
|
val codeChallenge = codeVerifier
|
||||||
|
val request =
|
||||||
|
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$key&code_challenge=$codeChallenge&state=RequestID$requestId"
|
||||||
|
context.openBrowser(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mapper = JsonMapper.builder().addModule(KotlinModule())
|
||||||
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||||
|
|
||||||
|
private var requestId = 0
|
||||||
|
private var codeVerifier = ""
|
||||||
|
|
||||||
|
private fun Context.storeToken(response: String) {
|
||||||
|
try {
|
||||||
|
if (response != "") {
|
||||||
|
val token = mapper.readValue<ResponseToken>(response)
|
||||||
|
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
|
||||||
|
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
|
||||||
|
setKey(accountId, MAL_TOKEN_KEY, token.access_token)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.refreshToken() {
|
||||||
|
try {
|
||||||
|
val res = post(
|
||||||
|
"https://myanimelist.net/v1/oauth2/token",
|
||||||
|
data = mapOf(
|
||||||
|
"client_id" to key,
|
||||||
|
"grant_type" to "refresh_token",
|
||||||
|
"refresh_token" to getKey(
|
||||||
|
accountId,
|
||||||
|
MAL_REFRESH_TOKEN_KEY
|
||||||
|
)!!
|
||||||
|
)
|
||||||
|
).text
|
||||||
|
storeToken(res)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val allTitles = hashMapOf<Int, MalTitleHolder>()
|
||||||
|
|
||||||
|
data class MalList(
|
||||||
|
@JsonProperty("data") val data: List<Data>,
|
||||||
|
@JsonProperty("paging") val paging: Paging
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MainPicture(
|
||||||
|
@JsonProperty("medium") val medium: String,
|
||||||
|
@JsonProperty("large") val large: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Node(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
@JsonProperty("main_picture") val main_picture: MainPicture?,
|
||||||
|
@JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles,
|
||||||
|
@JsonProperty("media_type") val media_type: String,
|
||||||
|
@JsonProperty("num_episodes") val num_episodes: Int,
|
||||||
|
@JsonProperty("status") val status: String,
|
||||||
|
@JsonProperty("start_date") val start_date: String?,
|
||||||
|
@JsonProperty("end_date") val end_date: String?,
|
||||||
|
@JsonProperty("average_episode_duration") val average_episode_duration: Int,
|
||||||
|
@JsonProperty("synopsis") val synopsis: String,
|
||||||
|
@JsonProperty("mean") val mean: Double,
|
||||||
|
@JsonProperty("genres") val genres: List<Genres>?,
|
||||||
|
@JsonProperty("rank") val rank: Int,
|
||||||
|
@JsonProperty("popularity") val popularity: Int,
|
||||||
|
@JsonProperty("num_list_users") val num_list_users: Int,
|
||||||
|
@JsonProperty("num_favorites") val num_favorites: Int,
|
||||||
|
@JsonProperty("num_scoring_users") val num_scoring_users: Int,
|
||||||
|
@JsonProperty("start_season") val start_season: StartSeason?,
|
||||||
|
@JsonProperty("broadcast") val broadcast: Broadcast?,
|
||||||
|
@JsonProperty("nsfw") val nsfw: String,
|
||||||
|
@JsonProperty("created_at") val created_at: String,
|
||||||
|
@JsonProperty("updated_at") val updated_at: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ListStatus(
|
||||||
|
@JsonProperty("status") val status: String?,
|
||||||
|
@JsonProperty("score") val score: Int,
|
||||||
|
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
|
||||||
|
@JsonProperty("is_rewatching") val is_rewatching: Boolean,
|
||||||
|
@JsonProperty("updated_at") val updated_at: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
@JsonProperty("node") val node: Node,
|
||||||
|
@JsonProperty("list_status") val list_status: ListStatus?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Paging(
|
||||||
|
@JsonProperty("next") val next: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AlternativeTitles(
|
||||||
|
@JsonProperty("synonyms") val synonyms: List<String>,
|
||||||
|
@JsonProperty("en") val en: String,
|
||||||
|
@JsonProperty("ja") val ja: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Genres(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("name") val name: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class StartSeason(
|
||||||
|
@JsonProperty("year") val year: Int,
|
||||||
|
@JsonProperty("season") val season: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Broadcast(
|
||||||
|
@JsonProperty("day_of_the_week") val day_of_the_week: String?,
|
||||||
|
@JsonProperty("start_time") val start_time: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.getMalAnimeListCached(): Array<Data>? {
|
||||||
|
return getKey(MAL_CACHED_LIST) as? Array<Data>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getMalAnimeListSmart(): Array<Data>? {
|
||||||
|
if (getKey<String>(
|
||||||
|
accountId,
|
||||||
|
MAL_TOKEN_KEY
|
||||||
|
) == null
|
||||||
|
) return null
|
||||||
|
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) {
|
||||||
|
val list = getMalAnimeList()
|
||||||
|
if (list != null) {
|
||||||
|
setKey(MAL_CACHED_LIST, list)
|
||||||
|
setKey(MAL_SHOULD_UPDATE_LIST, false)
|
||||||
|
}
|
||||||
|
list
|
||||||
|
} else {
|
||||||
|
getMalAnimeListCached()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.getMalAnimeList(): Array<Data>? {
|
||||||
|
return try {
|
||||||
|
checkMalToken()
|
||||||
|
var offset = 0
|
||||||
|
val fullList = mutableListOf<Data>()
|
||||||
|
val offsetRegex = Regex("""offset=(\d+)""")
|
||||||
|
while (true) {
|
||||||
|
val data: MalList = getMalAnimeListSlice(offset) ?: break
|
||||||
|
fullList.addAll(data.data)
|
||||||
|
offset = data.paging.next?.let { offsetRegex.find(it)?.groupValues?.get(1)?.toInt() } ?: break
|
||||||
|
}
|
||||||
|
fullList.toTypedArray()
|
||||||
|
//mapper.readValue<MalAnime>(res)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertToStatus(string: String): MalStatusType {
|
||||||
|
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.getMalAnimeListSlice(offset: Int = 0): MalList? {
|
||||||
|
val user = "@me"
|
||||||
|
return try {
|
||||||
|
// Very lackluster docs
|
||||||
|
// https://myanimelist.net/apiconfig/references/api/v2#operation/users_user_id_animelist_get
|
||||||
|
val url =
|
||||||
|
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status,num_episodes,media_type,status,start_date,end_date,synopsis,alternative_titles,mean,genres,rank,num_list_users,nsfw,average_episode_duration,num_favorites,popularity,num_scoring_users,start_season,favorites_info,broadcast,created_at,updated_at&nsfw=1&limit=100&offset=$offset"
|
||||||
|
val res = get(
|
||||||
|
url, headers = mapOf(
|
||||||
|
"Authorization" to "Bearer " + getKey<String>(
|
||||||
|
accountId,
|
||||||
|
MAL_TOKEN_KEY
|
||||||
|
)!!,
|
||||||
|
), cacheTime = 0
|
||||||
|
).text
|
||||||
|
res.toKotlinObject()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
val res = get(
|
||||||
|
url, headers = mapOf(
|
||||||
|
"Authorization" to "Bearer " + getKey<String>(
|
||||||
|
accountId,
|
||||||
|
MAL_TOKEN_KEY
|
||||||
|
)!!
|
||||||
|
), cacheTime = 0
|
||||||
|
).text
|
||||||
|
mapper.readValue<MalAnime>(res)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.setAllMalData() {
|
||||||
|
val user = "@me"
|
||||||
|
var isDone = false
|
||||||
|
var index = 0
|
||||||
|
allTitles.clear()
|
||||||
|
checkMalToken()
|
||||||
|
while (!isDone) {
|
||||||
|
val res = get(
|
||||||
|
"https://api.myanimelist.net/v2/users/$user/animelist?fields=list_status&limit=1000&offset=${index * 1000}",
|
||||||
|
headers = mapOf(
|
||||||
|
"Authorization" to "Bearer " + getKey<String>(
|
||||||
|
accountId,
|
||||||
|
MAL_TOKEN_KEY
|
||||||
|
)!!
|
||||||
|
), cacheTime = 0
|
||||||
|
).text
|
||||||
|
val values = mapper.readValue<MalRoot>(res)
|
||||||
|
val titles = values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
|
||||||
|
for (t in titles) {
|
||||||
|
allTitles[t.id] = t
|
||||||
|
}
|
||||||
|
isDone = titles.size < 1000
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
|
||||||
|
try {
|
||||||
|
// No time remaining if the show has already ended
|
||||||
|
try {
|
||||||
|
endDate?.let {
|
||||||
|
if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
|
||||||
|
}
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.checkMalToken() {
|
||||||
|
if (unixTime > getKey(
|
||||||
|
accountId,
|
||||||
|
MAL_UNIXTIME_KEY
|
||||||
|
) ?: 0L
|
||||||
|
) {
|
||||||
|
refreshToken()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.getMalUser(setSettings: Boolean = true): MalUser? {
|
||||||
|
checkMalToken()
|
||||||
|
return try {
|
||||||
|
val res = get(
|
||||||
|
"https://api.myanimelist.net/v2/users/@me",
|
||||||
|
headers = mapOf(
|
||||||
|
"Authorization" to "Bearer " + getKey<String>(
|
||||||
|
accountId,
|
||||||
|
MAL_TOKEN_KEY
|
||||||
|
)!!
|
||||||
|
), cacheTime = 0
|
||||||
|
).text
|
||||||
|
|
||||||
|
val user = mapper.readValue<MalUser>(res)
|
||||||
|
if (setSettings) {
|
||||||
|
setKey(accountId, MAL_USER_KEY, user)
|
||||||
|
}
|
||||||
|
user
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class MalStatusType(var value: Int) {
|
||||||
|
Watching(0),
|
||||||
|
Completed(1),
|
||||||
|
OnHold(2),
|
||||||
|
Dropped(3),
|
||||||
|
PlanToWatch(4),
|
||||||
|
None(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
|
||||||
|
return when (inp) {
|
||||||
|
-1 -> MalStatusType.None
|
||||||
|
0 -> MalStatusType.Watching
|
||||||
|
1 -> MalStatusType.Completed
|
||||||
|
2 -> MalStatusType.OnHold
|
||||||
|
3 -> MalStatusType.Dropped
|
||||||
|
4 -> MalStatusType.PlanToWatch
|
||||||
|
5 -> MalStatusType.Watching
|
||||||
|
else -> MalStatusType.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.setScoreRequest(
|
||||||
|
id: Int,
|
||||||
|
status: MalStatusType? = null,
|
||||||
|
score: Int? = null,
|
||||||
|
num_watched_episodes: Int? = null,
|
||||||
|
): Boolean {
|
||||||
|
val res = setScoreRequest(
|
||||||
|
id,
|
||||||
|
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
|
||||||
|
score,
|
||||||
|
num_watched_episodes
|
||||||
|
)
|
||||||
|
if (res != "") {
|
||||||
|
return try {
|
||||||
|
val malStatus = mapper.readValue<MalStatus>(res)
|
||||||
|
if (allTitles.containsKey(id)) {
|
||||||
|
val currentTitle = allTitles[id]!!
|
||||||
|
allTitles[id] = MalTitleHolder(malStatus, id, currentTitle.name)
|
||||||
|
} else {
|
||||||
|
allTitles[id] = MalTitleHolder(malStatus, id, "")
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.setScoreRequest(
|
||||||
|
id: Int,
|
||||||
|
status: String? = null,
|
||||||
|
score: Int? = null,
|
||||||
|
num_watched_episodes: Int? = null,
|
||||||
|
): String {
|
||||||
|
return try {
|
||||||
|
put(
|
||||||
|
"https://api.myanimelist.net/v2/anime/$id/my_list_status",
|
||||||
|
headers = mapOf(
|
||||||
|
"Authorization" to "Bearer " + getKey<String>(
|
||||||
|
accountId,
|
||||||
|
MAL_TOKEN_KEY
|
||||||
|
)!!
|
||||||
|
),
|
||||||
|
data = mapOf(
|
||||||
|
"status" to status,
|
||||||
|
"score" to score?.toString(),
|
||||||
|
"num_watched_episodes" to num_watched_episodes?.toString()
|
||||||
|
)
|
||||||
|
).text
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
data class ResponseToken(
|
||||||
|
@JsonProperty("token_type") val token_type: String,
|
||||||
|
@JsonProperty("expires_in") val expires_in: Int,
|
||||||
|
@JsonProperty("access_token") val access_token: String,
|
||||||
|
@JsonProperty("refresh_token") val refresh_token: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MalRoot(
|
||||||
|
@JsonProperty("data") val data: List<MalDatum>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MalDatum(
|
||||||
|
@JsonProperty("node") val node: MalNode,
|
||||||
|
@JsonProperty("list_status") val list_status: MalStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MalNode(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("title") val title: String,
|
||||||
|
/*
|
||||||
|
also, but not used
|
||||||
|
main_picture ->
|
||||||
|
public string medium;
|
||||||
|
public string large;
|
||||||
|
*/
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MalStatus(
|
||||||
|
@JsonProperty("status") val status: String,
|
||||||
|
@JsonProperty("score") val score: Int,
|
||||||
|
@JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
|
||||||
|
@JsonProperty("is_rewatching") val is_rewatching: Boolean,
|
||||||
|
@JsonProperty("updated_at") val updated_at: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MalUser(
|
||||||
|
@JsonProperty("id") val id: Int,
|
||||||
|
@JsonProperty("name") val name: String,
|
||||||
|
@JsonProperty("location") val location: String,
|
||||||
|
@JsonProperty("joined_at") val joined_at: String,
|
||||||
|
@JsonProperty("picture") val picture: 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?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MalTitleHolder(
|
||||||
|
val status: MalStatus,
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,7 +1,59 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
interface OAuth2Interface {
|
interface OAuth2Interface {
|
||||||
val key : String
|
val key : String
|
||||||
|
val name : String
|
||||||
val redirectUrl : String
|
val redirectUrl : String
|
||||||
fun handleRedirect(url : 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?,
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val malApi = MALApi("mal_account_0")
|
||||||
|
val aniListApi = AniListApi("anilist_account_0")
|
||||||
|
|
||||||
|
val OAuth2Apis get() = listOf(
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -26,6 +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.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
|
||||||
|
@ -48,6 +49,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact
|
import com.lagradost.cloudstream3.utils.UIHelper.getGridIsCompact
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager
|
import com.lagradost.cloudstream3.widget.CenterZoomLayoutManager
|
||||||
import kotlinx.android.synthetic.main.fragment_home.*
|
import kotlinx.android.synthetic.main.fragment_home.*
|
||||||
|
|
||||||
|
@ -420,5 +422,19 @@ class HomeFragment : Fragment() {
|
||||||
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
|
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
|
||||||
homeViewModel.loadAndCancel(apiName, currentPrefMedia)
|
homeViewModel.loadAndCancel(apiName, currentPrefMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nice profile pic on homepage
|
||||||
|
home_profile_picture_holder?.isVisible = false
|
||||||
|
context?.let { ctx ->
|
||||||
|
for (syncApi in OAuth2Interface.OAuth2Apis) {
|
||||||
|
val login = syncApi.loginInfo(ctx)
|
||||||
|
val pic = login?.profilePicture
|
||||||
|
if(pic != null) {
|
||||||
|
home_profile_picture.setImage(pic)
|
||||||
|
home_profile_picture_holder.isVisible = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings
|
package com.lagradost.cloudstream3.ui.settings
|
||||||
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.app.UiModeManager
|
import android.app.UiModeManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
@ -27,6 +29,9 @@ 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.OAuth2Interface.Companion.aniListApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.OAuth2Interface.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
|
||||||
|
@ -39,6 +44,7 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadDir
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadDir
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.isScopedStorage
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.isScopedStorage
|
||||||
|
@ -61,6 +67,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
|
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
|
||||||
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val accountEnabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var beneneCount = 0
|
private var beneneCount = 0
|
||||||
|
@ -111,6 +119,23 @@ 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 showLoginInfo(context: Context, api: OAuth2Interface, info: OAuth2Interface.LoginInfo) {
|
||||||
|
val builder =
|
||||||
|
AlertDialog.Builder(context, R.style.AlertDialogCustom).setView(R.layout.account_managment)
|
||||||
|
val dialog = builder.show()
|
||||||
|
|
||||||
|
dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture)
|
||||||
|
dialog.findViewById<TextView>(R.id.account_logout)?.setOnClickListener {
|
||||||
|
it.context?.let { ctx ->
|
||||||
|
api.logOut(ctx)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.findViewById<TextView>(R.id.account_name)?.text = info.name ?: context.getString(R.string.no_data)
|
||||||
|
dialog.findViewById<TextView>(R.id.account_site)?.text = api.name
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||||
|
@ -128,6 +153,27 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val preferedMediaTypePreference = findPreference<Preference>(getString(R.string.prefer_media_type_key))!!
|
val preferedMediaTypePreference = findPreference<Preference>(getString(R.string.prefer_media_type_key))!!
|
||||||
val appThemePreference = findPreference<Preference>(getString(R.string.app_theme_key))!!
|
val appThemePreference = findPreference<Preference>(getString(R.string.app_theme_key))!!
|
||||||
|
|
||||||
|
val syncApis = listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
|
||||||
|
for (sync in syncApis) {
|
||||||
|
findPreference<Preference>(getString(sync.first))?.apply {
|
||||||
|
isVisible = accountEnabled
|
||||||
|
val api = sync.second
|
||||||
|
title = getString(R.string.login_format).format(api.name, getString(R.string.account))
|
||||||
|
setOnPreferenceClickListener { pref ->
|
||||||
|
pref.context?.let { ctx ->
|
||||||
|
val info = api.loginInfo(ctx)
|
||||||
|
if (info != null) {
|
||||||
|
showLoginInfo(ctx, api, info)
|
||||||
|
} else {
|
||||||
|
api.authenticate(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return@setOnPreferenceClickListener true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
legalPreference.setOnPreferenceClickListener {
|
legalPreference.setOnPreferenceClickListener {
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
|
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
|
||||||
builder.setTitle(R.string.legal_notice)
|
builder.setTitle(R.string.legal_notice)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package com.lagradost.cloudstream3.utils
|
package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
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.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioFocusRequest
|
import android.media.AudioFocusRequest
|
||||||
|
@ -18,12 +20,11 @@ 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.MainAPI
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.R
|
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
|
||||||
import com.lagradost.cloudstream3.TvType
|
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
|
import java.net.URL
|
||||||
|
import java.net.URLDecoder
|
||||||
|
|
||||||
object AppUtils {
|
object AppUtils {
|
||||||
fun getVideoContentUri(context: Context, videoFilePath: String): Uri? {
|
fun getVideoContentUri(context: Context, videoFilePath: String): Uri? {
|
||||||
|
@ -44,6 +45,29 @@ object AppUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.openBrowser(url: String) {
|
||||||
|
val components = arrayOf(ComponentName(applicationContext, MainActivity::class.java))
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW)
|
||||||
|
intent.data = Uri.parse(url)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||||
|
startActivity(Intent.createChooser(intent, null).putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, components))
|
||||||
|
else
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun splitQuery(url: URL): Map<String, String> {
|
||||||
|
val queryPairs: MutableMap<String, String> = LinkedHashMap()
|
||||||
|
val query: String = url.query
|
||||||
|
val pairs = query.split("&").toTypedArray()
|
||||||
|
for (pair in pairs) {
|
||||||
|
val idx = pair.indexOf("=")
|
||||||
|
queryPairs[URLDecoder.decode(pair.substring(0, idx), "UTF-8")] =
|
||||||
|
URLDecoder.decode(pair.substring(idx + 1), "UTF-8")
|
||||||
|
}
|
||||||
|
return queryPairs
|
||||||
|
}
|
||||||
|
|
||||||
/**| S1:E2 Hello World
|
/**| S1:E2 Hello World
|
||||||
* | Episode 2. Hello world
|
* | Episode 2. Hello world
|
||||||
* | Hello World
|
* | Hello World
|
||||||
|
@ -82,7 +106,10 @@ object AppUtils {
|
||||||
fun AppCompatActivity.loadResult(url: String, apiName: String, startAction: Int = 0, startValue: Int = 0) {
|
fun AppCompatActivity.loadResult(url: String, apiName: String, startAction: Int = 0, startValue: Int = 0) {
|
||||||
this.runOnUiThread {
|
this.runOnUiThread {
|
||||||
// viewModelStore.clear()
|
// viewModelStore.clear()
|
||||||
this.navigate(R.id.global_to_navigation_results, ResultFragment.newInstance(url, apiName, startAction, startValue))
|
this.navigate(
|
||||||
|
R.id.global_to_navigation_results,
|
||||||
|
ResultFragment.newInstance(url, apiName, startAction, startValue)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,7 +224,7 @@ object AppUtils {
|
||||||
// Filter API depending on preferred media type
|
// Filter API depending on preferred media type
|
||||||
val listEnumAnime = listOf(TvType.Anime, TvType.AnimeMovie, TvType.ONA)
|
val listEnumAnime = listOf(TvType.Anime, TvType.AnimeMovie, TvType.ONA)
|
||||||
val listEnumMovieTv = listOf(TvType.Movie, TvType.TvSeries, TvType.Cartoon)
|
val listEnumMovieTv = listOf(TvType.Movie, TvType.TvSeries, TvType.Cartoon)
|
||||||
val mediaTypeList = if (currentPrefMedia==1) listEnumMovieTv else listEnumAnime
|
val mediaTypeList = if (currentPrefMedia == 1) listEnumMovieTv else listEnumAnime
|
||||||
|
|
||||||
val filteredAPI = allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } }
|
val filteredAPI = allApis.filter { api -> api.supportedTypes.any { it in mediaTypeList } }
|
||||||
filteredAPI
|
filteredAPI
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -13,6 +14,17 @@ object Coroutines {
|
||||||
work()
|
work()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ioSafe(work: suspend (() -> Unit)) : Job {
|
||||||
|
return CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
try {
|
||||||
|
work()
|
||||||
|
} catch (e : Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun runOnMainThread(work: (() -> Unit)) {
|
fun runOnMainThread(work: (() -> Unit)) {
|
||||||
val mainHandler = Handler(Looper.getMainLooper())
|
val mainHandler = Handler(Looper.getMainLooper())
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
|
|
16
app/src/main/res/drawable/ic_anilist_icon.xml
Normal file
16
app/src/main/res/drawable/ic_anilist_icon.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="172"
|
||||||
|
android:viewportHeight="172"
|
||||||
|
android:tint="?attr/white"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
android:pathData="M111.322,111.157L111.322,41.029C111.322,37.01 109.105,34.792 105.086,34.792L91.365,34.792C87.346,34.792 85.128,37.01 85.128,41.029C85.128,41.029 85.128,56.337 85.128,74.333C85.128,75.271 94.165,79.626 94.401,80.547C101.286,107.449 95.897,128.98 89.37,129.985C100.042,130.513 101.216,135.644 93.267,132.138C94.483,117.784 99.228,117.812 112.869,131.61C112.986,131.729 115.666,137.351 115.833,137.351C131.17,137.351 148.05,137.351 148.05,137.351C152.069,137.351 154.286,135.134 154.286,131.115L154.286,117.394C154.286,113.375 152.069,111.157 148.05,111.157L111.322,111.157Z"
|
||||||
|
android:fillColor="@color/black"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M54.365,34.792L18.331,137.351L46.327,137.351L52.425,119.611L82.915,119.611L88.875,137.351L116.732,137.351L80.836,34.792L54.365,34.792ZM58.8,96.882L67.531,68.47L77.094,96.882L58.8,96.882Z"
|
||||||
|
android:fillColor="@color/white"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
15
app/src/main/res/drawable/mal_logo.xml
Normal file
15
app/src/main/res/drawable/mal_logo.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:name="vector"
|
||||||
|
android:width="30dp"
|
||||||
|
android:height="30dp"
|
||||||
|
android:viewportWidth="850"
|
||||||
|
android:viewportHeight="850"
|
||||||
|
android:tint="?attr/white"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
android:name="path"
|
||||||
|
android:pathData="M 402 556 C 401.57 555.28 401 554.28 400.25 553 C 387.65 531.12 334.6 431 378.5 352.25 C 385.214 340.432 393.638 329.673 403.5 320.32 C 423 303 449 293.09 460.38 290.67 L 554.88 290.67 L 570 342.59 L 474 342.59 C 439.5 353.17 427.17 389 428 395.8 L 494.67 395.8 L 494.67 349 L 560.33 349 L 560.33 522.67 L 494.67 522.67 L 494.67 445.67 L 423 445.67 C 422.007 448.508 421.834 451.568 422.5 454.5 C 427.77 479.68 441.74 506.43 450 521 Z M 606.67 523.33 L 606.67 290.67 L 669.33 290.67 L 669.33 471.33 L 750.67 471.33 L 738 523.33 L 606.67 523.33 Z M 268.5 523.33 L 268.5 384 L 214 445.5 L 163.5 384 L 163.5 523.33 L 102.5 523.33 L 102.5 290.67 L 165.5 290.67 L 213.5 362.5 L 266.5 290.67 L 329 290.67 L 329 523.33 L 268.5 523.33 Z"
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1"/>
|
||||||
|
</vector>
|
65
app/src/main/res/layout/account_managment.xml
Normal file
65
app/src/main/res/layout/account_managment.xml
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingTop="20dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
app:cardCornerRadius="100dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="35dp">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/account_profile_picture"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:ignore="ContentDescription">
|
||||||
|
</ImageView>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
<LinearLayout
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent" android:layout_height="wrap_content">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/account_name"
|
||||||
|
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="MAL HelloWorld"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_rowWeight="1"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
</TextView>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/account_site"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
tools:text="MyAnimeList"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textColor="?attr/grayTextColor"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_rowWeight="1"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
</TextView>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/account_logout"
|
||||||
|
android:text="@string/logout"
|
||||||
|
style="@style/SettingsItem">
|
||||||
|
<requestFocus/>
|
||||||
|
</TextView>
|
||||||
|
</LinearLayout>
|
|
@ -105,16 +105,35 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="70dp">
|
android:layout_height="70dp">
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:gravity="center"
|
android:layout_marginEnd="50dp"
|
||||||
|
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
android:paddingBottom="10dp"
|
android:paddingBottom="10dp"
|
||||||
|
|
||||||
android:paddingStart="10dp"
|
android:paddingStart="10dp"
|
||||||
android:paddingEnd="10dp"
|
android:paddingEnd="10dp"
|
||||||
|
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<androidx.cardview.widget.CardView
|
||||||
|
android:id="@+id/home_profile_picture_holder"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
app:cardCornerRadius="100dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_width="35dp"
|
||||||
|
android:layout_height="35dp">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/home_profile_picture"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:ignore="ContentDescription">
|
||||||
|
</ImageView>
|
||||||
|
</androidx.cardview.widget.CardView>
|
||||||
|
<LinearLayout
|
||||||
|
android:gravity="center"
|
||||||
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_marginEnd="50dp"
|
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
<TextView
|
<TextView
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
|
@ -138,6 +157,8 @@
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
</TextView>
|
</TextView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:nextFocusDown="@id/home_main_poster_recyclerview"
|
android:nextFocusDown="@id/home_main_poster_recyclerview"
|
||||||
|
|
|
@ -310,4 +310,22 @@
|
||||||
|
|
||||||
<string name="primary_color_settings">Primary Color</string>
|
<string name="primary_color_settings">Primary Color</string>
|
||||||
<string name="app_theme_settings">App Theme</string>
|
<string name="app_theme_settings">App Theme</string>
|
||||||
|
|
||||||
|
<!-- account stuff -->
|
||||||
|
<string name="anilist_key" translatable="false">anilist_key</string>
|
||||||
|
<string name="mal_key" translatable="false">mal_key</string>
|
||||||
|
<!--
|
||||||
|
<string name="mal_account_settings" translatable="false">MAL</string>
|
||||||
|
<string name="anilist_account_settings" translatable="false">AniList</string>
|
||||||
|
<string name="tmdb_account_settings" translatable="false">TMDB</string>
|
||||||
|
<string name="imdb_account_settings" translatable="false">IMDB</string>
|
||||||
|
<string name="kitsu_account_settings" translatable="false">Kitsu</string>
|
||||||
|
<string name="trakt_account_settings" translatable="false">Trakt</string>
|
||||||
|
-->
|
||||||
|
<string name="login_format">%s %s</string>
|
||||||
|
<string name="account">account</string>
|
||||||
|
<string name="logout">Logout</string>
|
||||||
|
<string name="login">Login</string>
|
||||||
|
<!-- ============ -->
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -177,6 +177,17 @@
|
||||||
app:key="@string/manual_check_update_key"
|
app:key="@string/manual_check_update_key"
|
||||||
app:icon="@drawable/ic_baseline_system_update_24"
|
app:icon="@drawable/ic_baseline_system_update_24"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/mal_key"
|
||||||
|
android:icon="@drawable/mal_logo">
|
||||||
|
</Preference>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/anilist_key"
|
||||||
|
android:icon="@drawable/ic_anilist_icon">
|
||||||
|
</Preference>
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:title="@string/github"
|
android:title="@string/github"
|
||||||
android:icon="@drawable/ic_github_logo"
|
android:icon="@drawable/ic_github_logo"
|
||||||
|
|
Loading…
Reference in a new issue