Initial commit
This commit is contained in:
commit
21bfbfb139
520 changed files with 47819 additions and 0 deletions
39
app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt
Normal file
39
app/src/main/java/ani/dantotsu/connections/UpdateProgress.kt
Normal file
|
@ -0,0 +1,39 @@
|
|||
package ani.dantotsu.connections
|
||||
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.Refresh
|
||||
import ani.dantotsu.connections.anilist.Anilist
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun updateProgress(media: Media, number: String) {
|
||||
if (Anilist.userid != null) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val a = number.toFloatOrNull()?.roundToInt()
|
||||
if (a != media.userProgress) {
|
||||
Anilist.mutation.editList(
|
||||
media.id,
|
||||
a,
|
||||
status = if (media.userStatus == "REPEATING") media.userStatus else "CURRENT"
|
||||
)
|
||||
MAL.query.editList(
|
||||
media.idMAL,
|
||||
media.anime != null,
|
||||
a, null,
|
||||
if (media.userStatus == "REPEATING") media.userStatus!! else "CURRENT"
|
||||
)
|
||||
toast(currContext()?.getString(R.string.setting_progress, a))
|
||||
}
|
||||
media.userProgress = a
|
||||
Refresh.all()
|
||||
}
|
||||
} else {
|
||||
toast(currContext()?.getString(R.string.login_anilist_account))
|
||||
}
|
||||
}
|
143
app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
Normal file
143
app/src/main/java/ani/dantotsu/connections/anilist/Anilist.kt
Normal file
|
@ -0,0 +1,143 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.openLinkInBrowser
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
object Anilist {
|
||||
val query: AnilistQueries = AnilistQueries()
|
||||
val mutation: AnilistMutations = AnilistMutations()
|
||||
|
||||
var token: String? = null
|
||||
var username: String? = null
|
||||
var adult: Boolean = false
|
||||
var userid: Int? = null
|
||||
var avatar: String? = null
|
||||
var bg: String? = null
|
||||
var episodesWatched: Int? = null
|
||||
var chapterRead: Int? = null
|
||||
|
||||
var genres: ArrayList<String>? = null
|
||||
var tags: Map<Boolean, List<String>>? = null
|
||||
|
||||
val sortBy = listOf(
|
||||
"SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE"
|
||||
)
|
||||
|
||||
val seasons = listOf(
|
||||
"WINTER", "SPRING", "SUMMER", "FALL"
|
||||
)
|
||||
|
||||
val anime_formats = listOf(
|
||||
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
|
||||
)
|
||||
|
||||
val manga_formats = listOf(
|
||||
"MANGA", "NOVEL", "ONE SHOT"
|
||||
)
|
||||
|
||||
val authorRoles = listOf(
|
||||
"Original Creator", "Story & Art", "Story"
|
||||
)
|
||||
|
||||
private val cal: Calendar = Calendar.getInstance()
|
||||
private val currentYear = cal.get(Calendar.YEAR)
|
||||
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
|
||||
0, 1, 2 -> 0
|
||||
3, 4, 5 -> 1
|
||||
6, 7, 8 -> 2
|
||||
9, 10, 11 -> 3
|
||||
else -> 0
|
||||
}
|
||||
|
||||
private fun getSeason(next: Boolean): Pair<String, Int> {
|
||||
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
|
||||
var newYear = currentYear
|
||||
if (newSeason > 3) {
|
||||
newSeason = 0
|
||||
newYear++
|
||||
} else if (newSeason < 0) {
|
||||
newSeason = 3
|
||||
newYear--
|
||||
}
|
||||
return seasons[newSeason] to newYear
|
||||
}
|
||||
|
||||
val currentSeasons = listOf(
|
||||
getSeason(false),
|
||||
seasons[currentSeason] to currentYear,
|
||||
getSeason(true)
|
||||
)
|
||||
|
||||
fun loginIntent(context: Context) {
|
||||
val clientID = 14959
|
||||
try {
|
||||
CustomTabsIntent.Builder().build().launchUrl(
|
||||
context,
|
||||
Uri.parse("https://anilist.co/api/v2/oauth/authorize?client_id=$clientID&response_type=token")
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
openLinkInBrowser("https://anilist.co/api/v2/oauth/authorize?client_id=$clientID&response_type=token")
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
token = File(context.filesDir, "anilistToken").readText()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
token = null
|
||||
username = null
|
||||
adult = false
|
||||
userid = null
|
||||
avatar = null
|
||||
bg = null
|
||||
episodesWatched = null
|
||||
chapterRead = null
|
||||
if ("anilistToken" in context.fileList()) {
|
||||
File(context.filesDir, "anilistToken").delete()
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> executeQuery(
|
||||
query: String,
|
||||
variables: String = "",
|
||||
force: Boolean = false,
|
||||
useToken: Boolean = true,
|
||||
show: Boolean = false,
|
||||
cache: Int? = null
|
||||
): T? {
|
||||
return tryWithSuspend {
|
||||
val data = mapOf(
|
||||
"query" to query,
|
||||
"variables" to variables
|
||||
)
|
||||
val headers = mutableMapOf(
|
||||
"Content-Type" to "application/json",
|
||||
"Accept" to "application/json"
|
||||
)
|
||||
|
||||
if (token != null || force) {
|
||||
if (token != null && useToken) headers["Authorization"] = "Bearer $token"
|
||||
|
||||
val json = client.post("https://graphql.anilist.co/", headers, data = data, cacheTime = cache ?: 10)
|
||||
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
|
||||
if (show) println("Response : ${json.text}")
|
||||
json.parsed()
|
||||
} else null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
class AnilistMutations {
|
||||
|
||||
suspend fun toggleFav(anime: Boolean = true, id: Int) {
|
||||
val query =
|
||||
"""mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }"""
|
||||
val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
|
||||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
|
||||
suspend fun editList(
|
||||
mediaID: Int,
|
||||
progress: Int? = null,
|
||||
score: Int? = null,
|
||||
repeat: Int? = null,
|
||||
notes: String? = null,
|
||||
status: String? = null,
|
||||
private:Boolean? = null,
|
||||
startedAt: FuzzyDate? = null,
|
||||
completedAt: FuzzyDate? = null,
|
||||
customList: List<String>? = null
|
||||
) {
|
||||
|
||||
val query = """
|
||||
mutation ( ${"$"}mediaID: Int, ${"$"}progress: Int,${"$"}private:Boolean,${"$"}repeat: Int, ${"$"}notes: String, ${"$"}customLists: [String], ${"$"}scoreRaw:Int, ${"$"}status:MediaListStatus, ${"$"}start:FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, ${"$"}completed:FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} ) {
|
||||
SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) {
|
||||
score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day}
|
||||
}
|
||||
}
|
||||
""".replace("\n", "").replace(""" """, "")
|
||||
|
||||
val variables = """{"mediaID":$mediaID
|
||||
${if (private != null) ""","private":$private""" else ""}
|
||||
${if (progress != null) ""","progress":$progress""" else ""}
|
||||
${if (score != null) ""","scoreRaw":$score""" else ""}
|
||||
${if (repeat != null) ""","repeat":$repeat""" else ""}
|
||||
${if (notes != null) ""","notes":"${notes.replace("\n", "\\n")}"""" else ""}
|
||||
${if (status != null) ""","status":"$status"""" else ""}
|
||||
${if (customList !=null) ""","customLists":[${customList.joinToString { "\"$it\"" }}]""" else ""}
|
||||
}""".replace("\n", "").replace(""" """, "")
|
||||
println(variables)
|
||||
executeQuery<JsonObject>(query, variables, show = true)
|
||||
}
|
||||
|
||||
suspend fun deleteList(listId: Int) {
|
||||
val query = "mutation(${"$"}id:Int){DeleteMediaListEntry(id:${"$"}id){deleted}}"
|
||||
val variables = """{"id":"$listId"}"""
|
||||
executeQuery<JsonObject>(query, variables)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,925 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.app.Activity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.anilist.Anilist.authorRoles
|
||||
import ani.dantotsu.connections.anilist.Anilist.executeQuery
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.connections.anilist.api.Page
|
||||
import ani.dantotsu.connections.anilist.api.Query
|
||||
import ani.dantotsu.checkGenreTime
|
||||
import ani.dantotsu.checkId
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.media.Author
|
||||
import ani.dantotsu.media.Character
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.media.Studio
|
||||
import ani.dantotsu.others.MalScraper
|
||||
import ani.dantotsu.saveData
|
||||
import ani.dantotsu.snackString
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class AnilistQueries {
|
||||
suspend fun getUserData(): Boolean {
|
||||
val response: Query.Viewer?
|
||||
measureTimeMillis {
|
||||
response =
|
||||
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""")
|
||||
}.also { println("time : $it") }
|
||||
val user = response?.data?.user ?: return false
|
||||
|
||||
Anilist.userid = user.id
|
||||
Anilist.username = user.name
|
||||
Anilist.bg = user.bannerImage
|
||||
Anilist.avatar = user.avatar?.medium
|
||||
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
|
||||
Anilist.chapterRead = user.statistics?.manga?.chaptersRead
|
||||
Anilist.adult = user.options?.displayAdultContent ?: false
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun getMedia(id: Int, mal: Boolean = false): Media? {
|
||||
val response = executeQuery<Query.Media>(
|
||||
"""{Media(${if (!mal) "id:" else "idMal:"}$id){id idMal status chapters episodes nextAiringEpisode{episode}type meanScore isAdult isFavourite format bannerImage coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}""",
|
||||
force = true
|
||||
)
|
||||
val fetchedMedia = response?.data?.media ?: return null
|
||||
return Media(fetchedMedia)
|
||||
}
|
||||
|
||||
fun mediaDetails(media: Media): Media {
|
||||
media.cameFromContinue = false
|
||||
|
||||
val query =
|
||||
"""{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100) progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer { site id } synonyms tags { name rank isMediaSpoiler } characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100) status} episodes chapters nextAiringEpisode{episode} popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {edges{role node{id name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100) status} episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
|
||||
runBlocking {
|
||||
val anilist = async {
|
||||
var response = executeQuery<Query.Media>(query, force = true, show = true)
|
||||
if (response != null) {
|
||||
fun parse() {
|
||||
val fetchedMedia = response?.data?.media ?: return
|
||||
|
||||
media.source = fetchedMedia.source?.toString()
|
||||
media.countryOfOrigin = fetchedMedia.countryOfOrigin
|
||||
media.format = fetchedMedia.format?.toString()
|
||||
|
||||
media.startDate = fetchedMedia.startDate
|
||||
media.endDate = fetchedMedia.endDate
|
||||
|
||||
if (fetchedMedia.genres != null) {
|
||||
media.genres = arrayListOf()
|
||||
fetchedMedia.genres?.forEach { i ->
|
||||
media.genres.add(i)
|
||||
}
|
||||
}
|
||||
|
||||
media.trailer = fetchedMedia.trailer?.let { i ->
|
||||
if (i.site != null && i.site.toString() == "youtube")
|
||||
"https://www.youtube.com/embed/${i.id.toString().trim('"')}"
|
||||
else null
|
||||
}
|
||||
|
||||
fetchedMedia.synonyms?.apply {
|
||||
media.synonyms = arrayListOf()
|
||||
this.forEach { i ->
|
||||
media.synonyms.add(
|
||||
i
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fetchedMedia.tags?.apply {
|
||||
media.tags = arrayListOf()
|
||||
this.forEach { i ->
|
||||
if (i.isMediaSpoiler == false)
|
||||
media.tags.add("${i.name} : ${i.rank.toString()}%")
|
||||
}
|
||||
}
|
||||
|
||||
media.description = fetchedMedia.description.toString()
|
||||
|
||||
if (fetchedMedia.characters != null) {
|
||||
media.characters = arrayListOf()
|
||||
fetchedMedia.characters?.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
media.characters?.add(
|
||||
Character(
|
||||
id = id,
|
||||
name = i.node?.name?.userPreferred,
|
||||
image = i.node?.image?.medium,
|
||||
banner = media.banner ?: media.cover,
|
||||
role = when (i.role.toString()){
|
||||
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN"
|
||||
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING"
|
||||
else -> i.role.toString()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (fetchedMedia.relations != null) {
|
||||
media.relations = arrayListOf()
|
||||
fetchedMedia.relations?.edges?.forEach { mediaEdge ->
|
||||
val m = Media(mediaEdge)
|
||||
media.relations?.add(m)
|
||||
if (m.relation == "SEQUEL") {
|
||||
media.sequel = if ((media.sequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.sequel
|
||||
|
||||
} else if (m.relation == "PREQUEL") {
|
||||
media.prequel =
|
||||
if ((media.prequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.prequel
|
||||
}
|
||||
}
|
||||
media.relations?.sortByDescending { it.popularity }
|
||||
media.relations?.sortByDescending { it.startDate?.year }
|
||||
media.relations?.sortBy { it.relation }
|
||||
}
|
||||
if (fetchedMedia.recommendations != null) {
|
||||
media.recommendations = arrayListOf()
|
||||
fetchedMedia.recommendations?.nodes?.forEach { i ->
|
||||
i.mediaRecommendation?.apply {
|
||||
media.recommendations?.add(
|
||||
Media(this)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fetchedMedia.mediaListEntry != null) {
|
||||
fetchedMedia.mediaListEntry?.apply {
|
||||
media.userProgress = progress
|
||||
media.isListPrivate = private ?: false
|
||||
media.notes = notes
|
||||
media.userListId = id
|
||||
media.userScore = score?.toInt() ?: 0
|
||||
media.userStatus = status?.toString()
|
||||
media.inCustomListsOf = customLists?.toMutableMap()
|
||||
media.userRepeat = repeat ?: 0
|
||||
media.userUpdatedAt = updatedAt?.toString()?.toLong()?.times(1000)
|
||||
media.userCompletedAt = completedAt ?: FuzzyDate()
|
||||
media.userStartedAt = startedAt ?: FuzzyDate()
|
||||
}
|
||||
} else {
|
||||
media.isListPrivate = false
|
||||
media.userStatus = null
|
||||
media.userListId = null
|
||||
media.userProgress = null
|
||||
media.userScore = 0
|
||||
media.userRepeat = 0
|
||||
media.userUpdatedAt = null
|
||||
media.userCompletedAt = FuzzyDate()
|
||||
media.userStartedAt = FuzzyDate()
|
||||
}
|
||||
|
||||
if (media.anime != null) {
|
||||
media.anime.episodeDuration = fetchedMedia.duration
|
||||
media.anime.season = fetchedMedia.season?.toString()
|
||||
media.anime.seasonYear = fetchedMedia.seasonYear
|
||||
|
||||
fetchedMedia.studios?.nodes?.apply {
|
||||
if (isNotEmpty()) {
|
||||
val firstStudio = get(0)
|
||||
media.anime.mainStudio = Studio(
|
||||
firstStudio.id.toString(),
|
||||
firstStudio.name ?: "N/A"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||
media.anime.author = Author(
|
||||
it.id.toString(),
|
||||
it.name?.userPreferred ?: "N/A"
|
||||
)
|
||||
}
|
||||
|
||||
media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
|
||||
|
||||
fetchedMedia.externalLinks?.forEach { i ->
|
||||
when (i.site.lowercase()) {
|
||||
"youtube" -> media.anime.youtube = i.url
|
||||
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3)
|
||||
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (media.manga != null) {
|
||||
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
|
||||
media.manga.author = Author(
|
||||
it.id.toString(),
|
||||
it.name?.userPreferred ?: "N/A"
|
||||
)
|
||||
}
|
||||
}
|
||||
media.shareLink = fetchedMedia.siteUrl
|
||||
}
|
||||
|
||||
if (response.data?.media != null) parse()
|
||||
else {
|
||||
snackString(currContext()?.getString(R.string.adult_stuff))
|
||||
response = executeQuery(query, force = true, useToken = false)
|
||||
if (response?.data?.media != null) parse()
|
||||
else snackString(currContext()?.getString(R.string.what_did_you_open))
|
||||
}
|
||||
} else {
|
||||
snackString(currContext()?.getString(R.string.error_getting_data))
|
||||
}
|
||||
}
|
||||
val mal = async {
|
||||
if (media.idMAL != null) {
|
||||
MalScraper.loadMedia(media)
|
||||
}
|
||||
}
|
||||
awaitAll(anilist, mal)
|
||||
}
|
||||
return media
|
||||
}
|
||||
|
||||
suspend fun continueMedia(type: String,planned:Boolean=false): ArrayList<Media> {
|
||||
val returnArray = arrayListOf<Media>()
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
val statuses = if(!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING")
|
||||
suspend fun repeat(status: String) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||
|
||||
response?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
li.entries?.reversed()?.forEach {
|
||||
val m = Media(it)
|
||||
m.cameFromContinue = true
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses.forEach { repeat(it) }
|
||||
val set = loadData<MutableSet<Int>>("continue_$type")
|
||||
if (set != null) {
|
||||
set.reversed().forEach {
|
||||
if (map.containsKey(it)) returnArray.add(map[it]!!)
|
||||
}
|
||||
for (i in map) {
|
||||
if (i.value !in returnArray) returnArray.add(i.value)
|
||||
}
|
||||
} else returnArray.addAll(map.values)
|
||||
return returnArray
|
||||
}
|
||||
|
||||
suspend fun favMedia(anime: Boolean): ArrayList<Media> {
|
||||
var hasNextPage = true
|
||||
var page = 0
|
||||
|
||||
suspend fun getNextPage(page:Int): List<Media> {
|
||||
val response =
|
||||
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
|
||||
val favourites = response?.data?.user?.favourites
|
||||
val apiMediaList = if (anime) favourites?.anime else favourites?.manga
|
||||
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
|
||||
return apiMediaList?.edges?.mapNotNull {
|
||||
it.node?.let { i->
|
||||
Media(i).apply { isFav = true }
|
||||
}
|
||||
} ?: return listOf()
|
||||
}
|
||||
|
||||
val responseArray = arrayListOf<Media>()
|
||||
while(hasNextPage){
|
||||
page++
|
||||
responseArray.addAll(getNextPage(page))
|
||||
}
|
||||
return responseArray
|
||||
}
|
||||
|
||||
suspend fun recommendations(): ArrayList<Media> {
|
||||
val response =
|
||||
executeQuery<Query.Page>(""" { Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } } """)
|
||||
val map = mutableMapOf<Int, Media>()
|
||||
response?.data?.page?.apply {
|
||||
recommendations?.onEach {
|
||||
val json = it.mediaRecommendation
|
||||
if (json != null) {
|
||||
val m = Media(json)
|
||||
m.relation = json.type?.toString()
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val types = arrayOf("ANIME", "MANGA")
|
||||
suspend fun repeat(type: String) {
|
||||
val res =
|
||||
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """)
|
||||
res?.data?.mediaListCollection?.lists?.forEach { li ->
|
||||
li.entries?.forEach {
|
||||
val m = Media(it)
|
||||
if (m.status == "RELEASING" || m.status == "FINISHED") {
|
||||
m.relation = it.media?.type?.toString()
|
||||
map[m.id] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
types.forEach { repeat(it) }
|
||||
|
||||
val list = ArrayList(map.values.toList())
|
||||
list.sortByDescending { it.meanScore }
|
||||
return list
|
||||
}
|
||||
|
||||
private suspend fun bannerImage(type: String): String? {
|
||||
var image = loadData<BannerImage>("banner_$type")
|
||||
if (image == null || image.checkTime()) {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
|
||||
val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
|
||||
it.entries?.mapNotNull { entry ->
|
||||
val imageUrl = entry.media?.bannerImage
|
||||
if (imageUrl != null && imageUrl != "null") imageUrl
|
||||
else null
|
||||
}
|
||||
}?.flatten()?.randomOrNull() ?: return null
|
||||
|
||||
image = BannerImage(
|
||||
random,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
saveData("banner_$type", image)
|
||||
return image.url
|
||||
} else return image.url
|
||||
}
|
||||
|
||||
suspend fun getBannerImages(): ArrayList<String?> {
|
||||
val default = arrayListOf<String?>(null, null)
|
||||
default[0] = bannerImage("ANIME")
|
||||
default[1] = bannerImage("MANGA")
|
||||
return default
|
||||
}
|
||||
|
||||
suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap<String, ArrayList<Media>> {
|
||||
val response =
|
||||
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
|
||||
val sorted = mutableMapOf<String, ArrayList<Media>>()
|
||||
val unsorted = mutableMapOf<String, ArrayList<Media>>()
|
||||
val all = arrayListOf<Media>()
|
||||
val allIds = arrayListOf<Int>()
|
||||
|
||||
response?.data?.mediaListCollection?.lists?.forEach { i ->
|
||||
val name = i.name.toString().trim('"')
|
||||
unsorted[name] = arrayListOf()
|
||||
i.entries?.forEach {
|
||||
val a = Media(it)
|
||||
unsorted[name]?.add(a)
|
||||
if (!allIds.contains(a.id)) {
|
||||
allIds.add(a.id)
|
||||
all.add(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val options = response?.data?.mediaListCollection?.user?.mediaListOptions
|
||||
val mediaList = if (anime) options?.animeList else options?.mangaList
|
||||
mediaList?.sectionOrder?.forEach {
|
||||
if (unsorted.containsKey(it)) sorted[it] = unsorted[it]!!
|
||||
}
|
||||
unsorted.forEach {
|
||||
if(!sorted.containsKey(it.key)) sorted[it.key] = it.value
|
||||
}
|
||||
|
||||
sorted["Favourites"] = favMedia(anime)
|
||||
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
|
||||
|
||||
sorted["All"] = all
|
||||
|
||||
val sort = sortOrder ?: options?.rowOrder
|
||||
for (i in sorted.keys) {
|
||||
when (sort) {
|
||||
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) }
|
||||
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
|
||||
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
|
||||
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
|
||||
"id" -> sorted[i]?.sortWith(compareBy { it.id })
|
||||
}
|
||||
}
|
||||
return sorted
|
||||
}
|
||||
|
||||
|
||||
suspend fun getGenresAndTags(activity: Activity): Boolean {
|
||||
var genres: ArrayList<String>? = loadData("genres_list", activity)
|
||||
var tags: Map<Boolean, List<String>>? = loadData("tags_map", activity)
|
||||
|
||||
if (genres == null) {
|
||||
executeQuery<Query.GenreCollection>(
|
||||
"""{GenreCollection}""",
|
||||
force = true,
|
||||
useToken = false
|
||||
)?.data?.genreCollection?.apply {
|
||||
genres = arrayListOf()
|
||||
forEach {
|
||||
genres?.add(it)
|
||||
}
|
||||
saveData("genres_list", genres!!)
|
||||
}
|
||||
}
|
||||
if (tags == null) {
|
||||
executeQuery<Query.MediaTagCollection>(
|
||||
"""{ MediaTagCollection { name isAdult } }""",
|
||||
force = true
|
||||
)?.data?.mediaTagCollection?.apply {
|
||||
val adult = mutableListOf<String>()
|
||||
val good = mutableListOf<String>()
|
||||
forEach { node ->
|
||||
if (node.isAdult == true) adult.add(node.name)
|
||||
else good.add(node.name)
|
||||
}
|
||||
tags = mapOf(
|
||||
true to adult,
|
||||
false to good
|
||||
)
|
||||
saveData("tags_map", tags)
|
||||
}
|
||||
}
|
||||
return if (genres != null && tags != null) {
|
||||
Anilist.genres = genres
|
||||
Anilist.tags = tags
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
suspend fun getGenres(genres: ArrayList<String>, listener: ((Pair<String, String>) -> Unit)) {
|
||||
genres.forEach {
|
||||
getGenreThumbnail(it).apply {
|
||||
if (this != null) {
|
||||
listener.invoke(it to this.thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGenreThumbnail(genre: String): Genre? {
|
||||
val genres = loadData<MutableMap<String, Genre>>("genre_thumb") ?: mutableMapOf()
|
||||
if (genres.checkGenreTime(genre)) {
|
||||
try {
|
||||
val genreQuery =
|
||||
"""{ Page(perPage: 10){media(genre:"$genre", sort: TRENDING_DESC, type: ANIME, countryOfOrigin:"JP") {id bannerImage title{english romaji userPreferred} } } }"""
|
||||
executeQuery<Query.Page>(genreQuery, force = true)?.data?.page?.media?.forEach {
|
||||
if (genres.checkId(it.id) && it.bannerImage != null) {
|
||||
genres[genre] = Genre(
|
||||
genre,
|
||||
it.id,
|
||||
it.bannerImage!!,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
saveData("genre_thumb", genres)
|
||||
return genres[genre]
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
} else {
|
||||
return genres[genre]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun search(
|
||||
type: String,
|
||||
page: Int? = null,
|
||||
perPage: Int? = null,
|
||||
search: String? = null,
|
||||
sort: String? = null,
|
||||
genres: MutableList<String>? = null,
|
||||
tags: MutableList<String>? = null,
|
||||
format: String? = null,
|
||||
isAdult: Boolean = false,
|
||||
onList: Boolean? = null,
|
||||
excludedGenres: MutableList<String>? = null,
|
||||
excludedTags: MutableList<String>? = null,
|
||||
seasonYear: Int? = null,
|
||||
season: String? = null,
|
||||
id: Int? = null,
|
||||
hd: Boolean = false,
|
||||
): SearchResults? {
|
||||
val query = """
|
||||
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) {
|
||||
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
|
||||
pageInfo {
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
hasNextPage
|
||||
}
|
||||
media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode {
|
||||
episode
|
||||
}
|
||||
type
|
||||
genres
|
||||
meanScore
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
coverImage {
|
||||
large
|
||||
extraLarge
|
||||
}
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".replace("\n", " ").replace(""" """, "")
|
||||
val variables = """{"type":"$type","isAdult":$isAdult
|
||||
${if (onList != null) ""","onList":$onList""" else ""}
|
||||
${if (page != null) ""","page":"$page"""" else ""}
|
||||
${if (id != null) ""","id":"$id"""" else ""}
|
||||
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
|
||||
${if (season != null) ""","season":"$season"""" else ""}
|
||||
${if (search != null) ""","search":"$search"""" else ""}
|
||||
${if (sort!=null) ""","sort":"$sort"""" else ""}
|
||||
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
|
||||
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedGenres?.isNotEmpty() == true)
|
||||
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
||||
else ""
|
||||
}
|
||||
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
|
||||
${
|
||||
if (excludedTags?.isNotEmpty() == true)
|
||||
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]"""
|
||||
else ""
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
|
||||
if (response?.media != null) {
|
||||
val responseArray = arrayListOf<Media>()
|
||||
response.media?.forEach { i ->
|
||||
val userStatus = i.mediaListEntry?.status.toString()
|
||||
val genresArr = arrayListOf<String>()
|
||||
if (i.genres != null) {
|
||||
i.genres?.forEach { genre ->
|
||||
genresArr.add(genre)
|
||||
}
|
||||
}
|
||||
val media = Media(i)
|
||||
if (!hd) media.cover = i.coverImage?.large
|
||||
media.relation = if (onList == true) userStatus else null
|
||||
media.genres = genresArr
|
||||
responseArray.add(media)
|
||||
}
|
||||
|
||||
val pageInfo = response.pageInfo ?: return null
|
||||
|
||||
return SearchResults(
|
||||
type = type,
|
||||
perPage = perPage,
|
||||
search = search,
|
||||
sort = sort,
|
||||
isAdult = isAdult,
|
||||
onList = onList,
|
||||
genres = genres,
|
||||
excludedGenres = excludedGenres,
|
||||
tags = tags,
|
||||
excludedTags = excludedTags,
|
||||
format = format,
|
||||
seasonYear = seasonYear,
|
||||
season = season,
|
||||
results = responseArray,
|
||||
page = pageInfo.currentPage.toString().toIntOrNull() ?: 0,
|
||||
hasNextPage = pageInfo.hasNextPage == true,
|
||||
)
|
||||
} else snackString(currContext()?.getString(R.string.empty_response))
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun recentlyUpdated(
|
||||
smaller: Boolean = true,
|
||||
greater: Long = 0,
|
||||
lesser: Long = System.currentTimeMillis() / 1000 - 10000
|
||||
): MutableList<Media>? {
|
||||
suspend fun execute(page:Int = 1):Page?{
|
||||
val query = """{
|
||||
Page(page:$page,perPage:50) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
total
|
||||
}
|
||||
airingSchedules(
|
||||
airingAt_greater: $greater
|
||||
airingAt_lesser: $lesser
|
||||
sort:TIME_DESC
|
||||
) {
|
||||
episode
|
||||
airingAt
|
||||
media {
|
||||
id
|
||||
idMal
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
isAdult
|
||||
type
|
||||
meanScore
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
return executeQuery<Query.Page>(query, force = true)?.data?.page
|
||||
}
|
||||
if(smaller) {
|
||||
val response = execute()?.airingSchedules ?: return null
|
||||
val idArr = mutableListOf<Int>()
|
||||
val listOnly = loadData("recently_list_only") ?: false
|
||||
return response.mapNotNull { i ->
|
||||
i.media?.let {
|
||||
if (!idArr.contains(it.id))
|
||||
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) {
|
||||
idArr.add(it.id)
|
||||
Media(it)
|
||||
} else null
|
||||
else null
|
||||
}
|
||||
}.toMutableList()
|
||||
}else{
|
||||
var i = 1
|
||||
val list = mutableListOf<Media>()
|
||||
var res : Page? = null
|
||||
suspend fun next(){
|
||||
res = execute(i)
|
||||
list.addAll(res?.airingSchedules?.mapNotNull { j ->
|
||||
j.media?.let {
|
||||
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
|
||||
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
|
||||
} else null
|
||||
}
|
||||
}?: listOf())
|
||||
}
|
||||
next()
|
||||
while (res?.pageInfo?.hasNextPage == true){
|
||||
next()
|
||||
i++
|
||||
}
|
||||
return list.reversed().toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCharacterDetails(character: Character): Character {
|
||||
val query = """ {
|
||||
Character(id: ${character.id}) {
|
||||
id
|
||||
age
|
||||
gender
|
||||
description
|
||||
dateOfBirth {
|
||||
year
|
||||
month
|
||||
day
|
||||
}
|
||||
media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) {
|
||||
pageInfo {
|
||||
total
|
||||
perPage
|
||||
currentPage
|
||||
lastPage
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
id
|
||||
characterRole
|
||||
node {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
type
|
||||
meanScore
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
executeQuery<Query.Character>(query, force = true)?.data?.character?.apply {
|
||||
character.age = age
|
||||
character.gender = gender
|
||||
character.description = description
|
||||
character.dateOfBirth = dateOfBirth
|
||||
character.roles = arrayListOf()
|
||||
media?.edges?.forEach { i ->
|
||||
val m = Media(i)
|
||||
m.relation = i.characterRole.toString()
|
||||
character.roles?.add(m)
|
||||
}
|
||||
}
|
||||
return character
|
||||
}
|
||||
|
||||
suspend fun getStudioDetails(studio: Studio): Studio {
|
||||
fun query(page: Int = 0) = """ {
|
||||
Studio(id: ${studio.id}) {
|
||||
id
|
||||
media(page: $page,sort:START_DATE_DESC) {
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
id
|
||||
node {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
type
|
||||
meanScore
|
||||
startDate{ year }
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
var hasNextPage = true
|
||||
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
|
||||
var page = 0
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
val year = startDate?.year?.toString() ?: "TBA"
|
||||
val title = if (status != "CANCELLED") year else status
|
||||
if (!yearMedia.containsKey(title))
|
||||
yearMedia[title] = arrayListOf()
|
||||
yearMedia[title]?.add(Media(this))
|
||||
}
|
||||
}
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
}
|
||||
if (yearMedia.contains("CANCELLED")) {
|
||||
val a = yearMedia["CANCELLED"]!!
|
||||
yearMedia.remove("CANCELLED")
|
||||
yearMedia["CANCELLED"] = a
|
||||
}
|
||||
studio.yearMedia = yearMedia
|
||||
return studio
|
||||
}
|
||||
|
||||
|
||||
suspend fun getAuthorDetails(author: Author): Author {
|
||||
fun query(page: Int = 0) = """ {
|
||||
Staff(id: ${author.id}) {
|
||||
id
|
||||
staffMedia(page: $page,sort:START_DATE_DESC) {
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
}
|
||||
edges {
|
||||
staffRole
|
||||
id
|
||||
node {
|
||||
id
|
||||
idMal
|
||||
isAdult
|
||||
status
|
||||
chapters
|
||||
episodes
|
||||
nextAiringEpisode { episode }
|
||||
type
|
||||
meanScore
|
||||
startDate{ year }
|
||||
isFavourite
|
||||
format
|
||||
bannerImage
|
||||
countryOfOrigin
|
||||
coverImage { large }
|
||||
title {
|
||||
english
|
||||
romaji
|
||||
userPreferred
|
||||
}
|
||||
mediaListEntry {
|
||||
progress
|
||||
private
|
||||
score(format: POINT_100)
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}""".replace("\n", " ").replace(""" """, "")
|
||||
|
||||
var hasNextPage = true
|
||||
val yearMedia = mutableMapOf<String, ArrayList<Media>>()
|
||||
var page = 0
|
||||
|
||||
while (hasNextPage) {
|
||||
page++
|
||||
hasNextPage = executeQuery<Query.Author>(query(page), force = true)?.data?.author?.staffMedia?.let {
|
||||
it.edges?.forEach { i ->
|
||||
i.node?.apply {
|
||||
val status = status.toString()
|
||||
val year = startDate?.year?.toString() ?: "TBA"
|
||||
val title = if (status != "CANCELLED") year else status
|
||||
if (!yearMedia.containsKey(title))
|
||||
yearMedia[title] = arrayListOf()
|
||||
val media = Media(this)
|
||||
media.relation = i.staffRole
|
||||
yearMedia[title]?.add(media)
|
||||
}
|
||||
}
|
||||
it.pageInfo?.hasNextPage == true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
if (yearMedia.contains("CANCELLED")) {
|
||||
val a = yearMedia["CANCELLED"]!!
|
||||
yearMedia.remove("CANCELLED")
|
||||
yearMedia["CANCELLED"] = a
|
||||
}
|
||||
author.yearMedia = yearMedia
|
||||
return author
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.content.Context
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord
|
||||
import ani.dantotsu.loadData
|
||||
import ani.dantotsu.connections.mal.MAL
|
||||
import ani.dantotsu.media.Media
|
||||
import ani.dantotsu.others.AppUpdater
|
||||
import ani.dantotsu.snackString
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
suspend fun getUserId(context: Context, block: () -> Unit) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (Discord.userid == null && Discord.token != null) {
|
||||
if (!Discord.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_discord_user_data))
|
||||
}
|
||||
}
|
||||
|
||||
val anilist = if (Anilist.userid == null && Anilist.token != null) {
|
||||
if (Anilist.query.getUserData()) {
|
||||
tryWithSuspend {
|
||||
if (MAL.token != null && !MAL.query.getUserData())
|
||||
snackString(context.getString(R.string.error_loading_mal_user_data))
|
||||
}
|
||||
true
|
||||
} else {
|
||||
snackString(context.getString(R.string.error_loading_anilist_user_data))
|
||||
false
|
||||
}
|
||||
} else true
|
||||
|
||||
if(anilist) block.invoke()
|
||||
}
|
||||
|
||||
class AnilistHomeViewModel : ViewModel() {
|
||||
private val listImages: MutableLiveData<ArrayList<String?>> = MutableLiveData<ArrayList<String?>>(arrayListOf())
|
||||
fun getListImages(): LiveData<ArrayList<String?>> = listImages
|
||||
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
|
||||
|
||||
private val animeContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
|
||||
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
|
||||
|
||||
private val animeFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
|
||||
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
|
||||
|
||||
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
|
||||
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
|
||||
|
||||
private val mangaContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
|
||||
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
|
||||
|
||||
private val mangaFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
|
||||
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
|
||||
|
||||
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
|
||||
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
|
||||
|
||||
private val recommendation: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null)
|
||||
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
|
||||
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
|
||||
|
||||
suspend fun loadMain(context: FragmentActivity) {
|
||||
Anilist.getSavedToken(context)
|
||||
MAL.getSavedToken(context)
|
||||
Discord.getSavedToken(context)
|
||||
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context)
|
||||
genres.postValue(Anilist.query.getGenresAndTags(context))
|
||||
}
|
||||
|
||||
val empty = MutableLiveData<Boolean>(null)
|
||||
|
||||
var loaded: Boolean = false
|
||||
val genres: MutableLiveData<Boolean?> = MutableLiveData(null)
|
||||
}
|
||||
|
||||
class AnilistAnimeViewModel : ViewModel() {
|
||||
var searched = false
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
private val type = "ANIME"
|
||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||
suspend fun loadTrending(i: Int) {
|
||||
val (season, year) = Anilist.currentSeasons[i]
|
||||
trending.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
perPage = 12,
|
||||
sort = Anilist.sortBy[2],
|
||||
season = season,
|
||||
seasonYear = year,
|
||||
hd = true
|
||||
)?.results
|
||||
)
|
||||
}
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
fun getUpdated(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
|
||||
|
||||
private val animePopular = MutableLiveData<SearchResults?>(null)
|
||||
fun getPopular(): LiveData<SearchResults?> = animePopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
) {
|
||||
animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
suspend fun loadNextPage(r: SearchResults) = animePopular.postValue(
|
||||
Anilist.query.search(
|
||||
r.type,
|
||||
r.page + 1,
|
||||
r.perPage,
|
||||
r.search,
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.format,
|
||||
r.isAdult,
|
||||
r.onList
|
||||
)
|
||||
)
|
||||
|
||||
var loaded: Boolean = false
|
||||
}
|
||||
|
||||
class AnilistMangaViewModel : ViewModel() {
|
||||
var searched = false
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
private val type = "MANGA"
|
||||
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
fun getTrending(): LiveData<MutableList<Media>> = trending
|
||||
suspend fun loadTrending() =
|
||||
trending.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], hd = true)?.results)
|
||||
|
||||
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
|
||||
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
|
||||
suspend fun loadTrendingNovel() =
|
||||
updated.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], format = "NOVEL")?.results)
|
||||
|
||||
private val mangaPopular = MutableLiveData<SearchResults?>(null)
|
||||
fun getPopular(): LiveData<SearchResults?> = mangaPopular
|
||||
suspend fun loadPopular(
|
||||
type: String,
|
||||
search_val: String? = null,
|
||||
genres: ArrayList<String>? = null,
|
||||
sort: String = Anilist.sortBy[1],
|
||||
onList: Boolean = true,
|
||||
) {
|
||||
mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
type,
|
||||
search = search_val,
|
||||
onList = if (onList) null else false,
|
||||
sort = sort,
|
||||
genres = genres
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue(
|
||||
Anilist.query.search(
|
||||
r.type,
|
||||
r.page + 1,
|
||||
r.perPage,
|
||||
r.search,
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.format,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
)
|
||||
)
|
||||
|
||||
var loaded: Boolean = false
|
||||
}
|
||||
|
||||
class AnilistSearch : ViewModel() {
|
||||
var searched = false
|
||||
var notSet = true
|
||||
lateinit var searchResults: SearchResults
|
||||
private val result: MutableLiveData<SearchResults?> = MutableLiveData<SearchResults?>(null)
|
||||
|
||||
fun getSearch(): LiveData<SearchResults?> = result
|
||||
suspend fun loadSearch(r: SearchResults) = result.postValue(
|
||||
Anilist.query.search(
|
||||
r.type,
|
||||
r.page,
|
||||
r.perPage,
|
||||
r.search,
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.format,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun loadNextPage(r: SearchResults) = result.postValue(
|
||||
Anilist.query.search(
|
||||
r.type,
|
||||
r.page + 1,
|
||||
r.perPage,
|
||||
r.search,
|
||||
r.sort,
|
||||
r.genres,
|
||||
r.tags,
|
||||
r.format,
|
||||
r.isAdult,
|
||||
r.onList,
|
||||
r.excludedGenres,
|
||||
r.excludedTags,
|
||||
r.seasonYear,
|
||||
r.season
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class GenresViewModel : ViewModel() {
|
||||
var genres: MutableMap<String, String>? = null
|
||||
var done = false
|
||||
var doneListener: (() -> Unit)? = null
|
||||
suspend fun loadGenres(genre: ArrayList<String>, listener: (Pair<String, String>) -> Unit) {
|
||||
if (genres == null) {
|
||||
genres = mutableMapOf()
|
||||
Anilist.query.getGenres(genre) {
|
||||
genres!![it.first] = it.second
|
||||
listener.invoke(it)
|
||||
if (genres!!.size == genre.size) {
|
||||
done = true
|
||||
doneListener?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class BannerImage(
|
||||
val url: String?,
|
||||
var time: Long,
|
||||
) : Serializable {
|
||||
fun checkTime(): Boolean {
|
||||
return (System.currentTimeMillis() - time) >= (1000 * 60 * 60 * 6)
|
||||
}
|
||||
}
|
10
app/src/main/java/ani/dantotsu/connections/anilist/Genre.kt
Normal file
10
app/src/main/java/ani/dantotsu/connections/anilist/Genre.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
data class Genre(
|
||||
val name: String,
|
||||
var id: Int,
|
||||
var thumbnail: String,
|
||||
var time: Long,
|
||||
) : Serializable
|
27
app/src/main/java/ani/dantotsu/connections/anilist/Login.kt
Normal file
27
app/src/main/java/ani/dantotsu/connections/anilist/Login.kt
Normal file
|
@ -0,0 +1,27 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.logError
|
||||
import ani.dantotsu.logger
|
||||
import ani.dantotsu.startMainActivity
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val data: Uri? = intent?.data
|
||||
logger(data.toString())
|
||||
try {
|
||||
Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
|
||||
val filename = "anilistToken"
|
||||
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
|
||||
it.write(Anilist.token!!.toByteArray())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
startMainActivity(this)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
import ani.dantotsu.media.Media
|
||||
import java.io.Serializable
|
||||
|
||||
data class SearchResults(
|
||||
val type: String,
|
||||
var isAdult: Boolean,
|
||||
var onList: Boolean? = null,
|
||||
var perPage: Int? = null,
|
||||
var search: String? = null,
|
||||
var sort: String? = null,
|
||||
var genres: MutableList<String>? = null,
|
||||
var excludedGenres: MutableList<String>? = null,
|
||||
var tags: MutableList<String>? = null,
|
||||
var excludedTags: MutableList<String>? = null,
|
||||
var format: String? = null,
|
||||
var seasonYear: Int? = null,
|
||||
var season: String? = null,
|
||||
var page: Int = 1,
|
||||
var results: MutableList<Media>,
|
||||
var hasNextPage: Boolean,
|
||||
) : Serializable {
|
||||
fun toChipList(): List<SearchChip> {
|
||||
val list = mutableListOf<SearchChip>()
|
||||
sort?.let {
|
||||
val c = currContext()!!
|
||||
list.add(SearchChip("SORT", c.getString(R.string.filter_sort, c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)])))
|
||||
}
|
||||
format?.let {
|
||||
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
|
||||
}
|
||||
season?.let {
|
||||
list.add(SearchChip("SEASON", it))
|
||||
}
|
||||
seasonYear?.let {
|
||||
list.add(SearchChip("SEASON_YEAR", it.toString()))
|
||||
}
|
||||
genres?.forEach {
|
||||
list.add(SearchChip("GENRE", it))
|
||||
}
|
||||
excludedGenres?.forEach {
|
||||
list.add(SearchChip("EXCLUDED_GENRE", currContext()!!.getString(R.string.filter_exclude, it)))
|
||||
}
|
||||
tags?.forEach {
|
||||
list.add(SearchChip("TAG", it))
|
||||
}
|
||||
excludedTags?.forEach {
|
||||
list.add(SearchChip("EXCLUDED_TAG", currContext()!!.getString(R.string.filter_exclude, it)))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
fun removeChip(chip: SearchChip) {
|
||||
when (chip.type) {
|
||||
"SORT" -> sort = null
|
||||
"FORMAT" -> format = null
|
||||
"SEASON" -> season = null
|
||||
"SEASON_YEAR" -> seasonYear = null
|
||||
"GENRE" -> genres?.remove(chip.text)
|
||||
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)
|
||||
"TAG" -> tags?.remove(chip.text)
|
||||
"EXCLUDED_TAG" -> excludedTags?.remove(chip.text)
|
||||
}
|
||||
}
|
||||
|
||||
data class SearchChip(
|
||||
val type: String,
|
||||
val text: String
|
||||
)
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package ani.dantotsu.connections.anilist
|
||||
|
||||
import android.app.Activity
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import ani.dantotsu.loadMedia
|
||||
import ani.dantotsu.startMainActivity
|
||||
|
||||
class UrlMedia : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
|
||||
var isMAL = false
|
||||
var continueMedia = true
|
||||
if (id == 0) {
|
||||
continueMedia = false
|
||||
val data: Uri? = intent?.data
|
||||
isMAL = data?.host != "anilist.co"
|
||||
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
|
||||
} else loadMedia = id
|
||||
startMainActivity(this, bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Character(
|
||||
// The id of the character
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// The names of the character
|
||||
@SerialName("name") var name: CharacterName?,
|
||||
|
||||
// Character images
|
||||
@SerialName("image") var image: CharacterImage?,
|
||||
|
||||
// A general description of the character
|
||||
@SerialName("description") var description: String?,
|
||||
|
||||
// The character's gender. Usually Male, Female, or Non-binary but can be any string.
|
||||
@SerialName("gender") var gender: String?,
|
||||
|
||||
// The character's birth date
|
||||
@SerialName("dateOfBirth") var dateOfBirth: FuzzyDate?,
|
||||
|
||||
// The character's age. Note this is a string, not an int, it may contain further text and additional ages.
|
||||
@SerialName("age") var age: String?,
|
||||
|
||||
// The characters blood type
|
||||
@SerialName("bloodType") var bloodType: String?,
|
||||
|
||||
// If the character is marked as favourite by the currently authenticated user
|
||||
@SerialName("isFavourite") var isFavourite: Boolean?,
|
||||
|
||||
// If the character is blocked from being added to favourites
|
||||
@SerialName("isFavouriteBlocked") var isFavouriteBlocked: Boolean?,
|
||||
|
||||
// The url for the character page on the AniList website
|
||||
@SerialName("siteUrl") var siteUrl: String?,
|
||||
|
||||
// Media that includes the character
|
||||
@SerialName("media") var media: MediaConnection?,
|
||||
|
||||
// The amount of user's who have favourited the character
|
||||
@SerialName("favourites") var favourites: Int?,
|
||||
|
||||
// Notes for site moderators
|
||||
@SerialName("modNotes") var modNotes: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CharacterConnection(
|
||||
@SerialName("edges") var edges: List<CharacterEdge>?,
|
||||
|
||||
@SerialName("nodes") var nodes: List<Character>?,
|
||||
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CharacterEdge(
|
||||
@SerialName("node") var node: Character?,
|
||||
|
||||
// The id of the connection
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// The characters role in the media
|
||||
@SerialName("role") var role: String?,
|
||||
|
||||
// Media specific character name
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
// The voice actors of the character
|
||||
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
|
||||
// The voice actors of the character with role date
|
||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||
|
||||
// The media the character is in
|
||||
@SerialName("media") var media: List<Media>?,
|
||||
|
||||
// The order the character should be displayed from the users favourites
|
||||
@SerialName("favouriteOrder") var favouriteOrder: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CharacterName(
|
||||
// The character's given name
|
||||
@SerialName("first") var first: String?,
|
||||
|
||||
// The character's middle name
|
||||
@SerialName("middle") var middle: String?,
|
||||
|
||||
// The character's surname
|
||||
@SerialName("last") var last: String?,
|
||||
|
||||
// The character's first and last name
|
||||
@SerialName("full") var full: String?,
|
||||
|
||||
// The character's full name in their native language
|
||||
@SerialName("native") var native: String?,
|
||||
|
||||
// Other names the character might be referred to as
|
||||
@SerialName("alternative") var alternative: List<String>?,
|
||||
|
||||
// Other names the character might be referred to as but are spoilers
|
||||
@SerialName("alternativeSpoiler") var alternativeSpoiler: List<String>?,
|
||||
|
||||
// The currently authenticated users preferred name language. Default romaji for non-authenticated
|
||||
@SerialName("userPreferred") var userPreferred: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CharacterImage(
|
||||
// The character's image of media at its largest size
|
||||
@SerialName("large") var large: String?,
|
||||
|
||||
// The character's image of media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
)
|
185
app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
Normal file
185
app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt
Normal file
|
@ -0,0 +1,185 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class Query{
|
||||
@Serializable
|
||||
data class Viewer(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Viewer")
|
||||
val user: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
}
|
||||
@Serializable
|
||||
data class Media(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Media")
|
||||
val media: ani.dantotsu.connections.anilist.api.Media?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Page(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Page")
|
||||
val page : ani.dantotsu.connections.anilist.api.Page?
|
||||
)
|
||||
}
|
||||
// data class AiringSchedule(
|
||||
// val data : Data?
|
||||
// ){
|
||||
// data class Data(
|
||||
// val AiringSchedule: ani.dantotsu.connections.anilist.api.AiringSchedule?
|
||||
// )
|
||||
// }
|
||||
|
||||
@Serializable
|
||||
data class Character(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Character")
|
||||
val character: ani.dantotsu.connections.anilist.api.Character?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Studio(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Studio")
|
||||
val studio: ani.dantotsu.connections.anilist.api.Studio?
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
data class Author(
|
||||
@SerialName("data")
|
||||
val data: Data?
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("Staff")
|
||||
val author: Staff?
|
||||
)
|
||||
}
|
||||
|
||||
// data class MediaList(
|
||||
// val data: Data?
|
||||
// ){
|
||||
// data class Data(
|
||||
// val MediaList: ani.dantotsu.connections.anilist.api.MediaList?
|
||||
// )
|
||||
// }
|
||||
|
||||
@Serializable
|
||||
data class MediaListCollection(
|
||||
@SerialName("data")
|
||||
val data : Data?
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaListCollection")
|
||||
val mediaListCollection: ani.dantotsu.connections.anilist.api.MediaListCollection?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class GenreCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("GenreCollection")
|
||||
val genreCollection: List<String>?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaTagCollection(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("MediaTagCollection")
|
||||
val mediaTagCollection: List<MediaTag>?
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
@SerialName("data")
|
||||
val data: Data
|
||||
){
|
||||
@Serializable
|
||||
data class Data(
|
||||
@SerialName("User")
|
||||
val user: ani.dantotsu.connections.anilist.api.User?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//data class WhaData(
|
||||
// val Studio: Studio?,
|
||||
//
|
||||
// // Follow query
|
||||
// val Following: User?,
|
||||
//
|
||||
// // Follow query
|
||||
// val Follower: User?,
|
||||
//
|
||||
// // Thread query
|
||||
// val Thread: Thread?,
|
||||
//
|
||||
// // Recommendation query
|
||||
// val Recommendation: Recommendation?,
|
||||
//
|
||||
// // Like query
|
||||
// val Like: User?,
|
||||
|
||||
// // Review query
|
||||
// val Review: Review?,
|
||||
//
|
||||
// // Activity query
|
||||
// val Activity: ActivityUnion?,
|
||||
//
|
||||
// // Activity reply query
|
||||
// val ActivityReply: ActivityReply?,
|
||||
|
||||
// // Comment query
|
||||
// val ThreadComment: List<ThreadComment>?,
|
||||
|
||||
// // Notification query
|
||||
// val Notification: NotificationUnion?,
|
||||
|
||||
// // Media Trend query
|
||||
// val MediaTrend: MediaTrend?,
|
||||
|
||||
// // Provide AniList markdown to be converted to html (Requires auth)
|
||||
// val Markdown: ParsedMarkdown?,
|
||||
|
||||
// // SiteStatistics: SiteStatistics
|
||||
// val AniChartUser: AniChartUser?,
|
||||
//)
|
|
@ -0,0 +1,61 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import java.io.Serializable
|
||||
import java.text.DateFormatSymbols
|
||||
import java.util.*
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class FuzzyDate(
|
||||
@SerialName("year") val year: Int? = null,
|
||||
@SerialName("month") val month: Int? = null,
|
||||
@SerialName("day") val day: Int? = null,
|
||||
) : Serializable, Comparable<FuzzyDate> {
|
||||
|
||||
|
||||
fun isEmpty(): Boolean {
|
||||
return year == null && month == null && day == null
|
||||
}
|
||||
override fun toString(): String {
|
||||
return if ( isEmpty() ) "??" else toStringOrEmpty()
|
||||
}
|
||||
fun toStringOrEmpty(): String {
|
||||
return listOfNotNull(
|
||||
day?.toString(),
|
||||
month?.let { DateFormatSymbols().months.elementAt(it - 1) },
|
||||
year?.toString()
|
||||
).joinToString(" ")
|
||||
}
|
||||
|
||||
fun getToday(): FuzzyDate {
|
||||
val cal = Calendar.getInstance()
|
||||
return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH))
|
||||
}
|
||||
|
||||
fun toVariableString(): String {
|
||||
return listOfNotNull(
|
||||
year?.let {"year:$it"},
|
||||
month?.let {"month:$it"},
|
||||
day?.let {"day:$it"}
|
||||
).joinToString(",", "{", "}")
|
||||
}
|
||||
fun toMALString(): String {
|
||||
val padding = '0'
|
||||
val values = listOf(
|
||||
year?.toString()?.padStart(4, padding),
|
||||
month?.toString()?.padStart(2, padding),
|
||||
day?.toString()?.padStart(2, padding)
|
||||
)
|
||||
return values.takeWhile {it is String}.joinToString("-")
|
||||
}
|
||||
|
||||
// fun toInt(): Int {
|
||||
// return 10000 * (this.year ?: 0) + 100 * (this.month ?: 0) + (this.day ?: 0)
|
||||
// }
|
||||
|
||||
override fun compareTo(other: FuzzyDate): Int = when {
|
||||
year != other.year -> (year ?: 0) - (other.year ?: 0)
|
||||
month != other.month -> (month ?: 0) - (other.month ?: 0)
|
||||
else -> (day ?: 0) - (other.day ?: 0)
|
||||
}
|
||||
}
|
529
app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt
Normal file
529
app/src/main/java/ani/dantotsu/connections/anilist/api/Media.kt
Normal file
|
@ -0,0 +1,529 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.currContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Media(
|
||||
// The id of the media
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// The mal id of the media
|
||||
@SerialName("idMal") var idMal: Int?,
|
||||
|
||||
// The official titles of the media in various languages
|
||||
@SerialName("title") var title: MediaTitle?,
|
||||
|
||||
// The type of the media; anime or manga
|
||||
@SerialName("type") var type: MediaType?,
|
||||
|
||||
// The format the media was released in
|
||||
@SerialName("format") var format: MediaFormat?,
|
||||
|
||||
// The current releasing status of the media
|
||||
@SerialName("status") var status: MediaStatus?,
|
||||
|
||||
// Short description of the media's story and characters
|
||||
@SerialName("description") var description: String?,
|
||||
|
||||
// The first official release date of the media
|
||||
@SerialName("startDate") var startDate: FuzzyDate?,
|
||||
|
||||
// The last official release date of the media
|
||||
@SerialName("endDate") var endDate: FuzzyDate?,
|
||||
|
||||
// The season the media was initially released in
|
||||
@SerialName("season") var season: MediaSeason?,
|
||||
|
||||
// The season year the media was initially released in
|
||||
@SerialName("seasonYear") var seasonYear: Int?,
|
||||
|
||||
// The year & season the media was initially released in
|
||||
@SerialName("seasonInt") var seasonInt: Int?,
|
||||
|
||||
// The amount of episodes the anime has when complete
|
||||
@SerialName("episodes") var episodes: Int?,
|
||||
|
||||
// The general length of each anime episode in minutes
|
||||
@SerialName("duration") var duration: Int?,
|
||||
|
||||
// The amount of chapters the manga has when complete
|
||||
@SerialName("chapters") var chapters: Int?,
|
||||
|
||||
// The amount of volumes the manga has when complete
|
||||
@SerialName("volumes") var volumes: Int?,
|
||||
|
||||
// Where the media was created. (ISO 3166-1 alpha-2)
|
||||
// Originally a "CountryCode"
|
||||
@SerialName("countryOfOrigin") var countryOfOrigin: String?,
|
||||
|
||||
// If the media is officially licensed or a self-published doujin release
|
||||
@SerialName("isLicensed") var isLicensed: Boolean?,
|
||||
|
||||
// Source type the media was adapted from.
|
||||
@SerialName("source") var source: MediaSource?,
|
||||
|
||||
// Official Twitter hashtags for the media
|
||||
@SerialName("hashtag") var hashtag: String?,
|
||||
|
||||
// Media trailer or advertisement
|
||||
@SerialName("trailer") var trailer: MediaTrailer?,
|
||||
|
||||
// When the media's data was last updated
|
||||
@SerialName("updatedAt") var updatedAt: Int?,
|
||||
|
||||
// The cover images of the media
|
||||
@SerialName("coverImage") var coverImage: MediaCoverImage?,
|
||||
|
||||
// The banner image of the media
|
||||
@SerialName("bannerImage") var bannerImage: String?,
|
||||
|
||||
// The genres of the media
|
||||
@SerialName("genres") var genres: List<String>?,
|
||||
|
||||
// Alternative titles of the media
|
||||
@SerialName("synonyms") var synonyms: List<String>?,
|
||||
|
||||
// A weighted average score of all the user's scores of the media
|
||||
@SerialName("averageScore") var averageScore: Int?,
|
||||
|
||||
// Mean score of all the user's scores of the media
|
||||
@SerialName("meanScore") var meanScore: Int?,
|
||||
|
||||
// The number of users with the media on their list
|
||||
@SerialName("popularity") var popularity: Int?,
|
||||
|
||||
// Locked media may not be added to lists our favorited. This may be due to the entry pending for deletion or other reasons.
|
||||
@SerialName("isLocked") var isLocked: Boolean?,
|
||||
|
||||
// The amount of related activity in the past hour
|
||||
@SerialName("trending") var trending: Int?,
|
||||
|
||||
// The amount of user's who have favourited the media
|
||||
@SerialName("favourites") var favourites: Int?,
|
||||
|
||||
// List of tags that describes elements and themes of the media
|
||||
@SerialName("tags") var tags: List<MediaTag>?,
|
||||
|
||||
// Other media in the same or connecting franchise
|
||||
@SerialName("relations") var relations: MediaConnection?,
|
||||
|
||||
// The characters in the media
|
||||
@SerialName("characters") var characters: CharacterConnection?,
|
||||
|
||||
// The staff who produced the media
|
||||
@SerialName("staffPreview") var staff: StaffConnection?,
|
||||
|
||||
// The companies who produced the media
|
||||
@SerialName("studios") var studios: StudioConnection?,
|
||||
|
||||
// If the media is marked as favourite by the current authenticated user
|
||||
@SerialName("isFavourite") var isFavourite: Boolean?,
|
||||
|
||||
// If the media is blocked from being added to favourites
|
||||
@SerialName("isFavouriteBlocked") var isFavouriteBlocked: Boolean?,
|
||||
|
||||
// If the media is intended only for 18+ adult audiences
|
||||
@SerialName("isAdult") var isAdult: Boolean?,
|
||||
|
||||
// The media's next episode airing schedule
|
||||
@SerialName("nextAiringEpisode") var nextAiringEpisode: AiringSchedule?,
|
||||
|
||||
// The media's entire airing schedule
|
||||
// @SerialName("airingSchedule") var airingSchedule: AiringScheduleConnection?,
|
||||
|
||||
// The media's daily trend stats
|
||||
// @SerialName("trends") var trends: MediaTrendConnection?,
|
||||
|
||||
// External links to another site related to the media
|
||||
@SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?,
|
||||
|
||||
// Data and links to legal streaming episodes on external sites
|
||||
// @SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?,
|
||||
|
||||
// The ranking of the media in a particular time span and format compared to other media
|
||||
// @SerialName("rankings") var rankings: List<MediaRank>?,
|
||||
|
||||
// The authenticated user's media list entry for the media
|
||||
@SerialName("mediaListEntry") var mediaListEntry: MediaList?,
|
||||
|
||||
// User reviews of the media
|
||||
// @SerialName("reviews") var reviews: ReviewConnection?,
|
||||
|
||||
// User recommendations for similar media
|
||||
@SerialName("recommendations") var recommendations: RecommendationConnection?,
|
||||
|
||||
//
|
||||
// @SerialName("stats") var stats: MediaStats?,
|
||||
|
||||
// The url for the media page on the AniList website
|
||||
@SerialName("siteUrl") var siteUrl: String?,
|
||||
|
||||
// If the media should have forum thread automatically created for it on airing episode release
|
||||
@SerialName("autoCreateForumThread") var autoCreateForumThread: Boolean?,
|
||||
|
||||
// If the media is blocked from being recommended to/from
|
||||
@SerialName("isRecommendationBlocked") var isRecommendationBlocked: Boolean?,
|
||||
|
||||
// If the media is blocked from being reviewed
|
||||
@SerialName("isReviewBlocked") var isReviewBlocked: Boolean?,
|
||||
|
||||
// Notes for site moderators
|
||||
@SerialName("modNotes") var modNotes: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaTitle(
|
||||
// The romanization of the native language title
|
||||
@SerialName("romaji") var romaji: String,
|
||||
|
||||
// The official english title
|
||||
@SerialName("english") var english: String?,
|
||||
|
||||
// Official title in it's native language
|
||||
@SerialName("native") var native: String?,
|
||||
|
||||
// The currently authenticated users preferred title language. Default romaji for non-authenticated
|
||||
@SerialName("userPreferred") var userPreferred: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class MediaType {
|
||||
ANIME, MANGA;
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MediaStatus {
|
||||
FINISHED, RELEASING, NOT_YET_RELEASED, CANCELLED, HIATUS;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (super.toString()) {
|
||||
"FINISHED" -> currContext()!!.getString(R.string.status_finished)
|
||||
"RELEASING" -> currContext()!!.getString(R.string.status_releasing)
|
||||
"NOT_YET_RELEASED" -> currContext()!!.getString(R.string.status_not_yet_released)
|
||||
"CANCELLED" -> currContext()!!.getString(R.string.status_cancelled)
|
||||
"HIATUS" -> currContext()!!.getString(R.string.status_hiatus)
|
||||
else -> ""
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AiringSchedule(
|
||||
// The id of the airing schedule item
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// The time the episode airs at
|
||||
@SerialName("airingAt") var airingAt: Int?,
|
||||
|
||||
// Seconds until episode starts airing
|
||||
@SerialName("timeUntilAiring") var timeUntilAiring: Int?,
|
||||
|
||||
// The airing episode number
|
||||
@SerialName("episode") var episode: Int?,
|
||||
|
||||
// The associate media id of the airing episode
|
||||
@SerialName("mediaId") var mediaId: Int?,
|
||||
|
||||
// The associate media of the airing episode
|
||||
@SerialName("media") var media: Media?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaCoverImage(
|
||||
// The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.
|
||||
@SerialName("extraLarge") var extraLarge: String?,
|
||||
|
||||
// The cover image url of the media at a large size
|
||||
@SerialName("large") var large: String?,
|
||||
|
||||
// The cover image url of the media at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
|
||||
// Average #hex color of cover image
|
||||
@SerialName("color") var color: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaList(
|
||||
// The id of the list entry
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// The id of the user owner of the list entry
|
||||
@SerialName("userId") var userId: Int?,
|
||||
|
||||
// The id of the media
|
||||
@SerialName("mediaId") var mediaId: Int?,
|
||||
|
||||
// The watching/reading status
|
||||
@SerialName("status") var status: MediaListStatus?,
|
||||
|
||||
// The score of the entry
|
||||
@SerialName("score") var score: Float?,
|
||||
|
||||
// The amount of episodes/chapters consumed by the user
|
||||
@SerialName("progress") var progress: Int?,
|
||||
|
||||
// The amount of volumes read by the user
|
||||
@SerialName("progressVolumes") var progressVolumes: Int?,
|
||||
|
||||
// The amount of times the user has rewatched/read the media
|
||||
@SerialName("repeat") var repeat: Int?,
|
||||
|
||||
// Priority of planning
|
||||
@SerialName("priority") var priority: Int?,
|
||||
|
||||
// If the entry should only be visible to authenticated user
|
||||
@SerialName("private") var private: Boolean?,
|
||||
|
||||
// Text notes
|
||||
@SerialName("notes") var notes: String?,
|
||||
|
||||
// If the entry shown be hidden from non-custom lists
|
||||
@SerialName("hiddenFromStatusLists") var hiddenFromStatusLists: Boolean?,
|
||||
|
||||
// Map of booleans for which custom lists the entry are in
|
||||
@SerialName("customLists") var customLists: Map<String,Boolean>?,
|
||||
|
||||
// Map of advanced scores with name keys
|
||||
// @SerialName("advancedScores") var advancedScores: Json?,
|
||||
|
||||
// When the entry was started by the user
|
||||
@SerialName("startedAt") var startedAt: FuzzyDate?,
|
||||
|
||||
// When the entry was completed by the user
|
||||
@SerialName("completedAt") var completedAt: FuzzyDate?,
|
||||
|
||||
// When the entry data was last updated
|
||||
@SerialName("updatedAt") var updatedAt: Int?,
|
||||
|
||||
// When the entry data was created
|
||||
@SerialName("createdAt") var createdAt: Int?,
|
||||
|
||||
@SerialName("media") var media: Media?,
|
||||
|
||||
@SerialName("user") var user: User?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class MediaListStatus {
|
||||
CURRENT, PLANNING, COMPLETED, DROPPED, PAUSED, REPEATING;
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MediaSource {
|
||||
ORIGINAL, MANGA, LIGHT_NOVEL, VISUAL_NOVEL, VIDEO_GAME, OTHER, NOVEL, DOUJINSHI, ANIME, WEB_NOVEL, LIVE_ACTION, GAME, COMIC, MULTIMEDIA_PROJECT, PICTURE_BOOK;
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MediaFormat {
|
||||
TV, TV_SHORT, MOVIE, SPECIAL, OVA, ONA, MUSIC, MANGA, NOVEL, ONE_SHOT;
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaTrailer(
|
||||
// The trailer video id
|
||||
@SerialName("id") var id: String?,
|
||||
|
||||
// The site the video is hosted by (Currently either youtube or dailymotion)
|
||||
@SerialName("site") var site: String?,
|
||||
|
||||
// The url for the thumbnail image of the video
|
||||
@SerialName("thumbnail") var thumbnail: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaTagCollection(
|
||||
@SerialName("tags") var tags : List<MediaTag>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaTag(
|
||||
// The id of the tag
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// The name of the tag
|
||||
@SerialName("name") var name: String,
|
||||
|
||||
// A general description of the tag
|
||||
@SerialName("description") var description: String?,
|
||||
|
||||
// The categories of tags this tag belongs to
|
||||
@SerialName("category") var category: String?,
|
||||
|
||||
// The relevance ranking of the tag out of the 100 for this media
|
||||
@SerialName("rank") var rank: Int?,
|
||||
|
||||
// If the tag could be a spoiler for any media
|
||||
@SerialName("isGeneralSpoiler") var isGeneralSpoiler: Boolean?,
|
||||
|
||||
// If the tag is a spoiler for this media
|
||||
@SerialName("isMediaSpoiler") var isMediaSpoiler: Boolean?,
|
||||
|
||||
// If the tag is only for adult 18+ media
|
||||
@SerialName("isAdult") var isAdult: Boolean?,
|
||||
|
||||
// The user who submitted the tag
|
||||
@SerialName("userId") var userId: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaConnection(
|
||||
@SerialName("edges") var edges: List<MediaEdge>?,
|
||||
|
||||
@SerialName("nodes") var nodes: List<Media>?,
|
||||
|
||||
// The pagination information
|
||||
@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaEdge(
|
||||
//
|
||||
@SerialName("node") var node: Media?,
|
||||
|
||||
// The id of the connection
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// The type of relation to the parent model
|
||||
@SerialName("relationType") var relationType: MediaRelation?,
|
||||
|
||||
// If the studio is the main animation studio of the media (For Studio->MediaConnection field only)
|
||||
@SerialName("isMainStudio") var isMainStudio: Boolean?,
|
||||
|
||||
// The characters in the media voiced by the parent actor
|
||||
@SerialName("characters") var characters: List<Character>?,
|
||||
|
||||
// The characters role in the media
|
||||
@SerialName("characterRole") var characterRole: String?,
|
||||
|
||||
// Media specific character name
|
||||
@SerialName("characterName") var characterName: String?,
|
||||
|
||||
// Notes regarding the VA's role for the character
|
||||
@SerialName("roleNotes") var roleNotes: String?,
|
||||
|
||||
// Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant.
|
||||
@SerialName("dubGroup") var dubGroup: String?,
|
||||
|
||||
// The role of the staff member in the production of the media
|
||||
@SerialName("staffRole") var staffRole: String?,
|
||||
|
||||
// The voice actors of the character
|
||||
// @SerialName("voiceActors") var voiceActors: List<Staff>?,
|
||||
|
||||
// The voice actors of the character with role date
|
||||
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,
|
||||
|
||||
// The order the media should be displayed from the users favourites
|
||||
@SerialName("favouriteOrder") var favouriteOrder: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class MediaRelation {
|
||||
ADAPTATION, PREQUEL, SEQUEL, PARENT, SIDE_STORY, CHARACTER, SUMMARY, ALTERNATIVE, SPIN_OFF, OTHER, SOURCE, COMPILATION, CONTAINS;
|
||||
|
||||
override fun toString(): String {
|
||||
return when (super.toString()) {
|
||||
"ADAPTATION" -> currContext()!!.getString(R.string.type_adaptation)
|
||||
"PARENT" -> currContext()!!.getString(R.string.type_parent)
|
||||
"CHARACTER" -> currContext()!!.getString(R.string.type_character)
|
||||
"SUMMARY" -> currContext()!!.getString(R.string.type_summary)
|
||||
"ALTERNATIVE" -> currContext()!!.getString(R.string.type_alternative)
|
||||
"OTHER" -> currContext()!!.getString(R.string.type_other)
|
||||
"SOURCE" -> currContext()!!.getString(R.string.type_source)
|
||||
"CONTAINS" -> currContext()!!.getString(R.string.type_contains)
|
||||
else -> super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MediaSeason {
|
||||
WINTER, SPRING, SUMMER, FALL;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaExternalLink(
|
||||
// The id of the external link
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// The url of the external link or base url of link source
|
||||
@SerialName("url") var url: String?,
|
||||
|
||||
// The links website site name
|
||||
@SerialName("site") var site: String,
|
||||
|
||||
// The links website site id
|
||||
@SerialName("siteId") var siteId: Int?,
|
||||
|
||||
@SerialName("type") var type: ExternalLinkType?,
|
||||
|
||||
// Language the site content is in. See Staff language field for values.
|
||||
@SerialName("language") var language: String?,
|
||||
|
||||
@SerialName("color") var color: String?,
|
||||
|
||||
// The icon image url of the site. Not available for all links. Transparent PNG 64x64
|
||||
@SerialName("icon") var icon: String?,
|
||||
|
||||
// isDisabled: Boolean
|
||||
@SerialName("notes") var notes: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class ExternalLinkType {
|
||||
INFO, STREAMING, SOCIAL;
|
||||
|
||||
override fun toString(): String {
|
||||
return super.toString().replace("_", " ")
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MediaListCollection(
|
||||
// Grouped media list entries
|
||||
@SerialName("lists") var lists: List<MediaListGroup>?,
|
||||
|
||||
// The owner of the list
|
||||
@SerialName("user") var user: User?,
|
||||
|
||||
// If there is another chunk
|
||||
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
|
||||
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaListGroup(
|
||||
// Media list entries
|
||||
@SerialName("entries") var entries: List<MediaList>?,
|
||||
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
@SerialName("isCustomList") var isCustomList: Boolean?,
|
||||
|
||||
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
|
||||
|
||||
@SerialName("status") var status: MediaListStatus?,
|
||||
)
|
|
@ -0,0 +1,64 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Page(
|
||||
// The pagination information
|
||||
@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
|
||||
@SerialName("users") var users: List<User>?,
|
||||
|
||||
@SerialName("media") var media: List<Media>?,
|
||||
|
||||
@SerialName("characters") var characters: List<Character>?,
|
||||
|
||||
@SerialName("staff") var staff: List<Staff>?,
|
||||
|
||||
@SerialName("studios") var studios: List<Studio>?,
|
||||
|
||||
@SerialName("mediaList") var mediaList: List<MediaList>?,
|
||||
|
||||
@SerialName("airingSchedules") var airingSchedules: List<AiringSchedule>?,
|
||||
|
||||
// @SerialName("mediaTrends") var mediaTrends: List<MediaTrend>?,
|
||||
|
||||
// @SerialName("notifications") var notifications: List<NotificationUnion>?,
|
||||
|
||||
@SerialName("followers") var followers: List<User>?,
|
||||
|
||||
@SerialName("following") var following: List<User>?,
|
||||
|
||||
// @SerialName("activities") var activities: List<ActivityUnion>?,
|
||||
|
||||
// @SerialName("activityReplies") var activityReplies: List<ActivityReply>?,
|
||||
|
||||
// @SerialName("threads") var threads: List<Thread>?,
|
||||
|
||||
// @SerialName("threadComments") var threadComments: List<ThreadComment>?,
|
||||
|
||||
// @SerialName("reviews") var reviews: List<Review>?,
|
||||
|
||||
@SerialName("recommendations") var recommendations: List<Recommendation>?,
|
||||
|
||||
@SerialName("likes") var likes: List<User>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageInfo(
|
||||
// The total number of items. Note: This value is not guaranteed to be accurate, do not rely on this for logic
|
||||
@SerialName("total") var total: Int?,
|
||||
|
||||
// The count on a page
|
||||
@SerialName("perPage") var perPage: Int?,
|
||||
|
||||
// The current page
|
||||
@SerialName("currentPage") var currentPage: Int?,
|
||||
|
||||
// The last page
|
||||
@SerialName("lastPage") var lastPage: Int?,
|
||||
|
||||
// If there is another page
|
||||
@SerialName("hasNextPage") var hasNextPage: Boolean?,
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class Recommendation(
|
||||
// The id of the recommendation
|
||||
@SerialName("id") var id: Int?,
|
||||
|
||||
// Users rating of the recommendation
|
||||
@SerialName("rating") var rating: Int?,
|
||||
|
||||
// The rating of the recommendation by currently authenticated user
|
||||
// @SerialName("userRating") var userRating: RecommendationRating?,
|
||||
|
||||
// The media the recommendation is from
|
||||
@SerialName("media") var media: Media?,
|
||||
|
||||
// The recommended media
|
||||
@SerialName("mediaRecommendation") var mediaRecommendation: Media?,
|
||||
|
||||
// The user that first created the recommendation
|
||||
@SerialName("user") var user: User?,
|
||||
)
|
||||
@Serializable
|
||||
data class RecommendationConnection(
|
||||
//@SerialName("edges") var edges: List<RecommendationEdge>?,
|
||||
|
||||
@SerialName("nodes") var nodes: List<Recommendation>?,
|
||||
|
||||
// The pagination information
|
||||
//@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
|
||||
)
|
101
app/src/main/java/ani/dantotsu/connections/anilist/api/Staff.kt
Normal file
101
app/src/main/java/ani/dantotsu/connections/anilist/api/Staff.kt
Normal file
|
@ -0,0 +1,101 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Staff(
|
||||
// The id of the staff member
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// The names of the staff member
|
||||
@SerialName("name") var name: StaffName?,
|
||||
|
||||
// The primary language of the staff member. Current values: Japanese, English, Korean, Italian, Spanish, Portuguese, French, German, Hebrew, Hungarian, Chinese, Arabic, Filipino, Catalan, Finnish, Turkish, Dutch, Swedish, Thai, Tagalog, Malaysian, Indonesian, Vietnamese, Nepali, Hindi, Urdu
|
||||
@SerialName("languageV2") var languageV2: String?,
|
||||
|
||||
// The staff images
|
||||
// @SerialName("image") var image: StaffImage?,
|
||||
|
||||
// A general description of the staff member
|
||||
@SerialName("description") var description: String?,
|
||||
|
||||
// The person's primary occupations
|
||||
@SerialName("primaryOccupations") var primaryOccupations: List<String>?,
|
||||
|
||||
// The staff's gender. Usually Male, Female, or Non-binary but can be any string.
|
||||
@SerialName("gender") var gender: String?,
|
||||
|
||||
@SerialName("dateOfBirth") var dateOfBirth: FuzzyDate?,
|
||||
|
||||
@SerialName("dateOfDeath") var dateOfDeath: FuzzyDate?,
|
||||
|
||||
// The person's age in years
|
||||
@SerialName("age") var age: Int?,
|
||||
|
||||
// [startYear, endYear] (If the 2nd value is not present staff is still active)
|
||||
@SerialName("yearsActive") var yearsActive: List<Int>?,
|
||||
|
||||
// The persons birthplace or hometown
|
||||
@SerialName("homeTown") var homeTown: String?,
|
||||
|
||||
// The persons blood type
|
||||
@SerialName("bloodType") var bloodType: String?,
|
||||
|
||||
// If the staff member is marked as favourite by the currently authenticated user
|
||||
@SerialName("isFavourite") var isFavourite: Boolean?,
|
||||
|
||||
// If the staff member is blocked from being added to favourites
|
||||
@SerialName("isFavouriteBlocked") var isFavouriteBlocked: Boolean?,
|
||||
|
||||
// The url for the staff page on the AniList website
|
||||
@SerialName("siteUrl") var siteUrl: String?,
|
||||
|
||||
// Media where the staff member has a production role
|
||||
@SerialName("staffMedia") var staffMedia: MediaConnection?,
|
||||
|
||||
// Characters voiced by the actor
|
||||
@SerialName("characters") var characters: CharacterConnection?,
|
||||
|
||||
// Media the actor voiced characters in. (Same data as characters with media as node instead of characters)
|
||||
@SerialName("characterMedia") var characterMedia: MediaConnection?,
|
||||
|
||||
// Staff member that the submission is referencing
|
||||
@SerialName("staff") var staff: Staff?,
|
||||
|
||||
// Submitter for the submission
|
||||
@SerialName("submitter") var submitter: User?,
|
||||
|
||||
// Status of the submission
|
||||
@SerialName("submissionStatus") var submissionStatus: Int?,
|
||||
|
||||
// Inner details of submission status
|
||||
@SerialName("submissionNotes") var submissionNotes: String?,
|
||||
|
||||
// The amount of user's who have favourited the staff member
|
||||
@SerialName("favourites") var favourites: Int?,
|
||||
|
||||
// Notes for site moderators
|
||||
@SerialName("modNotes") var modNotes: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffName (
|
||||
var userPreferred:String?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffConnection(
|
||||
@SerialName("edges") var edges: List<StaffEdge>?,
|
||||
|
||||
@SerialName("nodes") var nodes: List<Staff>?,
|
||||
|
||||
// The pagination information
|
||||
// @SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StaffEdge(
|
||||
var role:String?,
|
||||
var node: Staff?
|
||||
)
|
|
@ -0,0 +1,39 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Studio(
|
||||
// The id of the studio
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// The name of the studio
|
||||
// Originally non-nullable, needs to be nullable due to it not being always queried
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
// If the studio is an animation studio or a different kind of company
|
||||
@SerialName("isAnimationStudio") var isAnimationStudio: Boolean?,
|
||||
|
||||
// The media the studio has worked on
|
||||
@SerialName("media") var media: MediaConnection?,
|
||||
|
||||
// The url for the studio page on the AniList website
|
||||
@SerialName("siteUrl") var siteUrl: String?,
|
||||
|
||||
// If the studio is marked as favourite by the currently authenticated user
|
||||
@SerialName("isFavourite") var isFavourite: Boolean?,
|
||||
|
||||
// The amount of user's who have favourited the studio
|
||||
@SerialName("favourites") var favourites: Int?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class StudioConnection(
|
||||
//@SerialName("edges") var edges: List<StudioEdge>?,
|
||||
|
||||
@SerialName("nodes") var nodes: List<Studio>?,
|
||||
|
||||
// The pagination information
|
||||
//@SerialName("pageInfo") var pageInfo: PageInfo?,
|
||||
)
|
196
app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt
Normal file
196
app/src/main/java/ani/dantotsu/connections/anilist/api/User.kt
Normal file
|
@ -0,0 +1,196 @@
|
|||
package ani.dantotsu.connections.anilist.api
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
// The id of the user
|
||||
@SerialName("id") var id: Int,
|
||||
|
||||
// The name of the user
|
||||
@SerialName("name") var name: String?,
|
||||
|
||||
// The bio written by user (Markdown)
|
||||
// @SerialName("about") var about: String?,
|
||||
|
||||
// The user's avatar images
|
||||
@SerialName("avatar") var avatar: UserAvatar?,
|
||||
|
||||
// The user's banner images
|
||||
@SerialName("bannerImage") var bannerImage: String?,
|
||||
|
||||
// If the authenticated user if following this user
|
||||
// @SerialName("isFollowing") var isFollowing: Boolean?,
|
||||
|
||||
// If this user if following the authenticated user
|
||||
// @SerialName("isFollower") var isFollower: Boolean?,
|
||||
|
||||
// If the user is blocked by the authenticated user
|
||||
// @SerialName("isBlocked") var isBlocked: Boolean?,
|
||||
|
||||
// FIXME: No documentation is provided for "Json"
|
||||
// @SerialName("bans") var bans: Json?,
|
||||
|
||||
// The user's general options
|
||||
@SerialName("options") var options: UserOptions?,
|
||||
|
||||
// The user's media list options
|
||||
@SerialName("mediaListOptions") var mediaListOptions: MediaListOptions?,
|
||||
|
||||
// The users favourites
|
||||
@SerialName("favourites") var favourites: Favourites?,
|
||||
|
||||
// The users anime & manga list statistics
|
||||
@SerialName("statistics") var statistics: UserStatisticTypes?,
|
||||
|
||||
// The number of unread notifications the user has
|
||||
// @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
|
||||
|
||||
// The url for the user page on the AniList website
|
||||
// @SerialName("siteUrl") var siteUrl: String?,
|
||||
|
||||
// The donation tier of the user
|
||||
// @SerialName("donatorTier") var donatorTier: Int?,
|
||||
|
||||
// Custom donation badge text
|
||||
// @SerialName("donatorBadge") var donatorBadge: String?,
|
||||
|
||||
// The user's moderator roles if they are a site moderator
|
||||
// @SerialName("moderatorRoles") var moderatorRoles: List<ModRole>?,
|
||||
|
||||
// When the user's account was created. (Does not exist for accounts created before 2020)
|
||||
// @SerialName("createdAt") var createdAt: Int?,
|
||||
|
||||
// When the user's data was last updated
|
||||
// @SerialName("updatedAt") var updatedAt: Int?,
|
||||
|
||||
// The user's previously used names.
|
||||
// @SerialName("previousNames") var previousNames: List<UserPreviousName>?,
|
||||
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserOptions(
|
||||
// The language the user wants to see media titles in
|
||||
// @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
|
||||
|
||||
// Whether the user has enabled viewing of 18+ content
|
||||
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
|
||||
|
||||
// Whether the user receives notifications when a show they are watching aires
|
||||
@SerialName("airingNotifications") var airingNotifications: Boolean?,
|
||||
//
|
||||
// Profile highlight color (blue, purple, pink, orange, red, green, gray)
|
||||
@SerialName("profileColor") var profileColor: String?,
|
||||
//
|
||||
// // Notification options
|
||||
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
|
||||
//
|
||||
// // The user's timezone offset (Auth user only)
|
||||
// @SerialName("timezone") var timezone: String?,
|
||||
//
|
||||
// // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
|
||||
// @SerialName("activityMergeTime") var activityMergeTime: Int?,
|
||||
//
|
||||
// // The language the user wants to see staff and character names in
|
||||
// // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
|
||||
//
|
||||
// // Whether the user only allow messages from users they follow
|
||||
// @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
|
||||
|
||||
// The list activity types the user has disabled from being created from list updates
|
||||
// @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserAvatar(
|
||||
// The avatar of user at its largest size
|
||||
@SerialName("large") var large: String?,
|
||||
|
||||
// The avatar of user at medium size
|
||||
@SerialName("medium") var medium: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserStatisticTypes(
|
||||
@SerialName("anime") var anime: UserStatistics?,
|
||||
@SerialName("manga") var manga: UserStatistics?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UserStatistics(
|
||||
//
|
||||
@SerialName("count") var count: Int?,
|
||||
@SerialName("meanScore") var meanScore: Float?,
|
||||
@SerialName("standardDeviation") var standardDeviation: Float?,
|
||||
@SerialName("minutesWatched") var minutesWatched: Int?,
|
||||
@SerialName("episodesWatched") var episodesWatched: Int?,
|
||||
@SerialName("chaptersRead") var chaptersRead: Int?,
|
||||
@SerialName("volumesRead") var volumesRead: Int?,
|
||||
// @SerialName("formats") var formats: List<UserFormatStatistic>?,
|
||||
// @SerialName("statuses") var statuses: List<UserStatusStatistic>?,
|
||||
// @SerialName("scores") var scores: List<UserScoreStatistic>?,
|
||||
// @SerialName("lengths") var lengths: List<UserLengthStatistic>?,
|
||||
// @SerialName("releaseYears") var releaseYears: List<UserReleaseYearStatistic>?,
|
||||
// @SerialName("startYears") var startYears: List<UserStartYearStatistic>?,
|
||||
// @SerialName("genres") var genres: List<UserGenreStatistic>?,
|
||||
// @SerialName("tags") var tags: List<UserTagStatistic>?,
|
||||
// @SerialName("countries") var countries: List<UserCountryStatistic>?,
|
||||
// @SerialName("voiceActors") var voiceActors: List<UserVoiceActorStatistic>?,
|
||||
// @SerialName("staff") var staff: List<UserStaffStatistic>?,
|
||||
// @SerialName("studios") var studios: List<UserStudioStatistic>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Favourites(
|
||||
// Favourite anime
|
||||
@SerialName("anime") var anime: MediaConnection?,
|
||||
|
||||
// Favourite manga
|
||||
@SerialName("manga") var manga: MediaConnection?,
|
||||
|
||||
// Favourite characters
|
||||
@SerialName("characters") var characters: CharacterConnection?,
|
||||
|
||||
// Favourite staff
|
||||
@SerialName("staff") var staff: StaffConnection?,
|
||||
|
||||
// Favourite studios
|
||||
@SerialName("studios") var studios: StudioConnection?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaListOptions(
|
||||
// The score format the user is using for media lists
|
||||
// @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
|
||||
|
||||
// The default order list rows should be displayed in
|
||||
@SerialName("rowOrder") var rowOrder: String?,
|
||||
|
||||
// The user's anime list options
|
||||
@SerialName("animeList") var animeList: MediaListTypeOptions?,
|
||||
|
||||
// The user's manga list options
|
||||
@SerialName("mangaList") var mangaList: MediaListTypeOptions?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MediaListTypeOptions(
|
||||
// The order each list should be displayed in
|
||||
@SerialName("sectionOrder") var sectionOrder: List<String>?,
|
||||
|
||||
// If the completed sections of the list should be separated by format
|
||||
@SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
|
||||
|
||||
// The names of the user's custom lists
|
||||
@SerialName("customLists") var customLists: List<String>?,
|
||||
//
|
||||
// // The names of the user's advanced scoring sections
|
||||
// @SerialName("advancedScoring") var advancedScoring: List<String>?,
|
||||
//
|
||||
// // If advanced scoring is enabled
|
||||
// @SerialName("advancedScoringEnabled") var advancedScoringEnabled: Boolean?,
|
||||
)
|
||||
|
112
app/src/main/java/ani/dantotsu/connections/discord/Discord.kt
Normal file
112
app/src/main/java/ani/dantotsu/connections/discord/Discord.kt
Normal file
|
@ -0,0 +1,112 @@
|
|||
package ani.dantotsu.connections.discord
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.edit
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.serializers.User
|
||||
import ani.dantotsu.others.CustomBottomDialog
|
||||
import ani.dantotsu.toast
|
||||
import ani.dantotsu.tryWith
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import java.io.File
|
||||
|
||||
object Discord {
|
||||
|
||||
var token: String? = null
|
||||
var userid: String? = null
|
||||
var avatar: String? = null
|
||||
|
||||
private const val TOKEN = "discord_token"
|
||||
|
||||
fun getSavedToken(context: Context): Boolean {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
token = sharedPref.getString(TOKEN, null)
|
||||
return token != null
|
||||
}
|
||||
|
||||
fun saveToken(context: Context, token: String) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
putString(TOKEN, token)
|
||||
commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences(
|
||||
context.getString(R.string.preference_file_key),
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
sharedPref.edit {
|
||||
remove(TOKEN)
|
||||
commit()
|
||||
}
|
||||
|
||||
tryWith(true) {
|
||||
val dir = File(context.filesDir?.parentFile, "app_webview")
|
||||
if (dir.deleteRecursively())
|
||||
toast(context.getString(R.string.discord_logout_success))
|
||||
}
|
||||
}
|
||||
|
||||
private var rpc : RPC? = null
|
||||
suspend fun getUserData() = tryWithSuspend(true) {
|
||||
if(rpc==null) {
|
||||
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
|
||||
val user: User = rpc.getUserData()
|
||||
userid = user.username
|
||||
avatar = user.userAvatar()
|
||||
rpc.close()
|
||||
true
|
||||
} else true
|
||||
} ?: false
|
||||
|
||||
|
||||
fun warning(context: Context) = CustomBottomDialog().apply {
|
||||
title = context.getString(R.string.warning)
|
||||
val md = context.getString(R.string.discord_warning)
|
||||
addView(TextView(context).apply {
|
||||
val markWon =
|
||||
Markwon.builder(context).usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
|
||||
markWon.setMarkdown(this, md)
|
||||
})
|
||||
|
||||
setNegativeButton(context.getString(R.string.cancel)) {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
setPositiveButton(context.getString(R.string.login)) {
|
||||
dismiss()
|
||||
loginIntent(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loginIntent(context: Context) {
|
||||
val intent = Intent(context, Login::class.java)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
fun defaultRPC(): RPC? {
|
||||
return token?.let {
|
||||
RPC(it, Dispatchers.IO).apply {
|
||||
applicationId = "1163925779692912771"
|
||||
smallImage = RPC.Link(
|
||||
"Dantotsu",
|
||||
"mp:attachments/1163940221063278672/1163940262423298141/bitmap1024.png"
|
||||
)
|
||||
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
51
app/src/main/java/ani/dantotsu/connections/discord/Login.kt
Normal file
51
app/src/main/java/ani/dantotsu/connections/discord/Login.kt
Normal file
|
@ -0,0 +1,51 @@
|
|||
package ani.dantotsu.connections.discord
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ani.dantotsu.R
|
||||
import ani.dantotsu.connections.discord.Discord.saveToken
|
||||
import ani.dantotsu.startMainActivity
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_discord)
|
||||
|
||||
val webView = findViewById<WebView>(R.id.discordWebview)
|
||||
webView.apply {
|
||||
settings.javaScriptEnabled = true
|
||||
settings.databaseEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
}
|
||||
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
if (url != null && url.endsWith("/app")) {
|
||||
webView.stopLoading()
|
||||
webView.evaluateJavascript("""
|
||||
(function() {
|
||||
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w])
|
||||
webpackChunkdiscord_app.pop()
|
||||
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
|
||||
return token;
|
||||
})()
|
||||
""".trimIndent()){
|
||||
login(it.trim('"'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
webView.loadUrl("https://discord.com/login")
|
||||
}
|
||||
|
||||
private fun login(token: String) {
|
||||
finish()
|
||||
saveToken(this, token)
|
||||
startMainActivity(this@Login)
|
||||
}
|
||||
|
||||
}
|
238
app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
Normal file
238
app/src/main/java/ani/dantotsu/connections/discord/RPC.kt
Normal file
|
@ -0,0 +1,238 @@
|
|||
package ani.dantotsu.connections.discord
|
||||
|
||||
import ani.dantotsu.connections.discord.serializers.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import kotlinx.serialization.json.long
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.util.concurrent.TimeUnit.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import ani.dantotsu.client as app
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
open class RPC(val token: String, val coroutineContext: CoroutineContext) {
|
||||
|
||||
private val json = Json {
|
||||
encodeDefaults = true
|
||||
allowStructuredMapKeys = true
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, SECONDS)
|
||||
.readTimeout(10, SECONDS)
|
||||
.writeTimeout(10, SECONDS)
|
||||
.build()
|
||||
|
||||
private val request = Request.Builder()
|
||||
.url("wss://gateway.discord.gg/?encoding=json&v=10")
|
||||
.build()
|
||||
|
||||
private var webSocket = client.newWebSocket(request, Listener())
|
||||
|
||||
var applicationId: String? = null
|
||||
var type: Type? = null
|
||||
var activityName: String? = null
|
||||
var details: String? = null
|
||||
var state: String? = null
|
||||
var largeImage: Link? = null
|
||||
var smallImage: Link? = null
|
||||
var status: String? = null
|
||||
var startTimestamp: Long? = null
|
||||
var stopTimestamp: Long? = null
|
||||
|
||||
enum class Type {
|
||||
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
|
||||
}
|
||||
|
||||
var buttons = mutableListOf<Link>()
|
||||
|
||||
data class Link(val label: String, val url: String)
|
||||
|
||||
private suspend fun createPresence(): String {
|
||||
return json.encodeToString(Presence.Response(
|
||||
3,
|
||||
Presence(
|
||||
activities = listOf(
|
||||
Activity(
|
||||
name = activityName,
|
||||
state = state,
|
||||
details = details,
|
||||
type = type?.ordinal,
|
||||
timestamps = if (startTimestamp != null)
|
||||
Activity.Timestamps(startTimestamp, stopTimestamp)
|
||||
else null,
|
||||
assets = Activity.Assets(
|
||||
largeImage = largeImage?.url?.discordUrl(),
|
||||
largeText = largeImage?.label,
|
||||
smallImage = smallImage?.url?.discordUrl(),
|
||||
smallText = smallImage?.label
|
||||
),
|
||||
buttons = buttons.map { it.label },
|
||||
metadata = Activity.Metadata(
|
||||
buttonUrls = buttons.map { it.url }
|
||||
),
|
||||
applicationId = applicationId,
|
||||
)
|
||||
),
|
||||
afk = true,
|
||||
since = startTimestamp,
|
||||
status = status
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class KizzyApi(val id: String)
|
||||
val api = "https://kizzy-api.vercel.app/image?url="
|
||||
private suspend fun String.discordUrl(): String? {
|
||||
if (startsWith("mp:")) return this
|
||||
val json = app.get("$api$this").parsedSafe<KizzyApi>()
|
||||
return json?.id
|
||||
}
|
||||
|
||||
private fun sendIdentify() {
|
||||
val response = Identity.Response(
|
||||
op = 2,
|
||||
d = Identity(
|
||||
token = token,
|
||||
properties = Identity.Properties(
|
||||
os = "windows",
|
||||
browser = "Chrome",
|
||||
device = "disco"
|
||||
),
|
||||
compress = false,
|
||||
intents = 0
|
||||
)
|
||||
)
|
||||
webSocket.send(json.encodeToString(response))
|
||||
}
|
||||
|
||||
fun send(block: RPC.() -> Unit) {
|
||||
block.invoke(this)
|
||||
send()
|
||||
}
|
||||
|
||||
var started = false
|
||||
var whenStarted: ((User) -> Unit)? = null
|
||||
|
||||
fun send() {
|
||||
val send = {
|
||||
CoroutineScope(coroutineContext).launch {
|
||||
webSocket.send(createPresence())
|
||||
}
|
||||
}
|
||||
if (!started) whenStarted = {
|
||||
send.invoke()
|
||||
whenStarted = null
|
||||
}
|
||||
else send.invoke()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
webSocket.send(
|
||||
json.encodeToString(
|
||||
Presence.Response(
|
||||
3,
|
||||
Presence(status = "offline")
|
||||
)
|
||||
)
|
||||
)
|
||||
webSocket.close(4000, "Interrupt")
|
||||
}
|
||||
|
||||
//I hate this, but couldn't find any better way to solve it
|
||||
suspend fun getUserData(): User {
|
||||
var user : User? = null
|
||||
whenStarted = {
|
||||
user = it
|
||||
whenStarted = null
|
||||
}
|
||||
while (user == null) {
|
||||
delay(100)
|
||||
}
|
||||
return user!!
|
||||
}
|
||||
|
||||
var onReceiveUserData: ((User) -> Deferred<Unit>)? = null
|
||||
|
||||
inner class Listener : WebSocketListener() {
|
||||
private var seq: Int? = null
|
||||
private var heartbeatInterval: Long? = null
|
||||
|
||||
var scope = CoroutineScope(coroutineContext)
|
||||
|
||||
private fun sendHeartBeat() {
|
||||
scope.cancel()
|
||||
scope = CoroutineScope(coroutineContext)
|
||||
scope.launch {
|
||||
delay(heartbeatInterval!!)
|
||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||
println("Message : $text")
|
||||
|
||||
val map = json.decodeFromString<Res>(text)
|
||||
seq = map.s
|
||||
|
||||
when (map.op) {
|
||||
10 -> {
|
||||
map.d as JsonObject
|
||||
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
|
||||
sendHeartBeat()
|
||||
sendIdentify()
|
||||
}
|
||||
|
||||
0 -> if (map.t == "READY") {
|
||||
val user = json.decodeFromString<User.Response>(text).d.user
|
||||
started = true
|
||||
whenStarted?.invoke(user)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
if (scope.isActive) scope.cancel()
|
||||
webSocket.send("{\"op\":1, \"d\":$seq}")
|
||||
}
|
||||
|
||||
11 -> sendHeartBeat()
|
||||
7 -> webSocket.close(400, "Reconnect")
|
||||
9 -> {
|
||||
sendHeartBeat()
|
||||
sendIdentify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||
println("Server Closed : $code $reason")
|
||||
if (code == 4000) {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
||||
println("Failure : ${t.message}")
|
||||
if (t.message != "Interrupt") {
|
||||
this@RPC.webSocket = client.newWebSocket(request, Listener())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@Serializable
|
||||
data class Activity (
|
||||
@SerialName("application_id")
|
||||
val applicationId: String? = null,
|
||||
val name: String? = null,
|
||||
val details: String? = null,
|
||||
val state: String? = null,
|
||||
val type: Int? = null,
|
||||
val timestamps: Timestamps? = null,
|
||||
val assets: Assets? = null,
|
||||
val buttons: List<String>? = null,
|
||||
val metadata: Metadata? = null
|
||||
) {
|
||||
@Serializable
|
||||
data class Assets(
|
||||
@SerialName("large_image")
|
||||
val largeImage: String? = null,
|
||||
|
||||
@SerialName("large_text")
|
||||
val largeText: String? = null,
|
||||
|
||||
@SerialName("small_image")
|
||||
val smallImage: String? = null,
|
||||
|
||||
@SerialName("small_text")
|
||||
val smallText: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Metadata(
|
||||
@SerialName("button_urls")
|
||||
val buttonUrls: List<String>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Timestamps(
|
||||
val start: Long? = null,
|
||||
val stop: Long? = null
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Identity(
|
||||
val token: String,
|
||||
val properties: Properties,
|
||||
val compress: Boolean,
|
||||
val intents: Long
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Response (
|
||||
val op: Long,
|
||||
val d: Identity
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Properties (
|
||||
@SerialName("\$os")
|
||||
val os: String,
|
||||
|
||||
@SerialName("\$browser")
|
||||
val browser: String,
|
||||
|
||||
@SerialName("\$device")
|
||||
val device: String
|
||||
)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Presence (
|
||||
val activities: List<Activity> = listOf(),
|
||||
val afk: Boolean = true,
|
||||
val since: Long? = null,
|
||||
val status: String? = null
|
||||
){
|
||||
@Serializable
|
||||
data class Response (
|
||||
val op: Long,
|
||||
val d: Presence
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
@Serializable
|
||||
data class Res(
|
||||
val t: String?,
|
||||
val s: Int?,
|
||||
val op: Int,
|
||||
val d: JsonElement
|
||||
)
|
|
@ -0,0 +1,76 @@
|
|||
package ani.dantotsu.connections.discord.serializers
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import kotlinx.serialization.json.*
|
||||
|
||||
@Serializable
|
||||
data class User (
|
||||
val verified: Boolean,
|
||||
val username: String,
|
||||
|
||||
@SerialName("purchased_flags")
|
||||
val purchasedFlags: Long,
|
||||
|
||||
@SerialName("public_flags")
|
||||
val publicFlags: Long,
|
||||
|
||||
val pronouns: String,
|
||||
|
||||
@SerialName("premium_type")
|
||||
val premiumType: Long,
|
||||
|
||||
val premium: Boolean,
|
||||
val phone: String,
|
||||
|
||||
@SerialName("nsfw_allowed")
|
||||
val nsfwAllowed: Boolean,
|
||||
|
||||
val mobile: Boolean,
|
||||
|
||||
@SerialName("mfa_enabled")
|
||||
val mfaEnabled: Boolean,
|
||||
|
||||
val id: String,
|
||||
|
||||
@SerialName("global_name")
|
||||
val globalName: String,
|
||||
|
||||
val flags: Long,
|
||||
val email: String,
|
||||
val discriminator: String,
|
||||
val desktop: Boolean,
|
||||
val bio: String,
|
||||
|
||||
@SerialName("banner_color")
|
||||
val bannerColor: String,
|
||||
|
||||
val banner: JsonElement? = null,
|
||||
|
||||
@SerialName("avatar_decoration")
|
||||
val avatarDecoration: JsonElement? = null,
|
||||
|
||||
val avatar: String,
|
||||
|
||||
@SerialName("accent_color")
|
||||
val accentColor: Long
|
||||
) {
|
||||
@Serializable
|
||||
data class Response(
|
||||
val t: String,
|
||||
val s: Long,
|
||||
val op: Long,
|
||||
val d: D
|
||||
) {
|
||||
@Serializable
|
||||
data class D(
|
||||
val v: Long,
|
||||
val user: User,
|
||||
)
|
||||
}
|
||||
|
||||
fun userAvatar():String{
|
||||
return "https://cdn.discordapp.com/avatars/$id/$avatar.png"
|
||||
}
|
||||
}
|
52
app/src/main/java/ani/dantotsu/connections/mal/Login.kt
Normal file
52
app/src/main/java/ani/dantotsu/connections/mal/Login.kt
Normal file
|
@ -0,0 +1,52 @@
|
|||
package ani.dantotsu.connections.mal
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import ani.dantotsu.*
|
||||
import ani.dantotsu.connections.mal.MAL.clientId
|
||||
import ani.dantotsu.connections.mal.MAL.saveResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class Login : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
try {
|
||||
val data: Uri = intent?.data
|
||||
?: throw Exception(getString(R.string.mal_login_uri_not_found))
|
||||
val codeChallenge: String = loadData("malCodeChallenge", this)
|
||||
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
|
||||
val code = data.getQueryParameter("code")
|
||||
?: throw Exception(getString(R.string.mal_login_code_not_present))
|
||||
|
||||
snackString(getString(R.string.logging_in_mal))
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
tryWithSuspend(true) {
|
||||
val res = client.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
data = mapOf(
|
||||
"client_id" to clientId,
|
||||
"code" to code,
|
||||
"code_verifier" to codeChallenge,
|
||||
"grant_type" to "authorization_code"
|
||||
)
|
||||
).parsed<MAL.ResponseToken>()
|
||||
saveResponse(res)
|
||||
MAL.token = res.accessToken
|
||||
snackString(getString(R.string.getting_user_data))
|
||||
MAL.query.getUserData()
|
||||
launch(Dispatchers.Main) {
|
||||
startMainActivity(this@Login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e:Exception){
|
||||
logError(e,snackbar = false)
|
||||
startMainActivity(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
99
app/src/main/java/ani/dantotsu/connections/mal/MAL.kt
Normal file
99
app/src/main/java/ani/dantotsu/connections/mal/MAL.kt
Normal file
|
@ -0,0 +1,99 @@
|
|||
package ani.dantotsu.connections.mal
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import ani.dantotsu.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
object MAL {
|
||||
val query: MALQueries = MALQueries()
|
||||
const val clientId = "86b35cf02205a0303da3aaea1c9e33f3"
|
||||
var username: String? = null
|
||||
var avatar: String? = null
|
||||
var token: String? = null
|
||||
var userid: Int? = null
|
||||
|
||||
fun loginIntent(context: Context) {
|
||||
val codeVerifierBytes = ByteArray(96)
|
||||
SecureRandom().nextBytes(codeVerifierBytes)
|
||||
val codeChallenge = Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=')
|
||||
.replace("+", "-")
|
||||
.replace("/", "_")
|
||||
.replace("\n", "")
|
||||
|
||||
saveData("malCodeChallenge", codeChallenge, context)
|
||||
val request =
|
||||
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
|
||||
try {
|
||||
CustomTabsIntent.Builder().build().launchUrl(
|
||||
context,
|
||||
Uri.parse(request)
|
||||
)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
openLinkInBrowser(request)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAL_TOKEN = "malToken"
|
||||
|
||||
private suspend fun refreshToken(): ResponseToken? {
|
||||
return tryWithSuspend {
|
||||
val token = loadData<ResponseToken>(MAL_TOKEN)
|
||||
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
|
||||
val res = client.post(
|
||||
"https://myanimelist.net/v1/oauth2/token",
|
||||
data = mapOf(
|
||||
"client_id" to clientId,
|
||||
"grant_type" to "refresh_token",
|
||||
"refresh_token" to token.refreshToken
|
||||
)
|
||||
).parsed<ResponseToken>()
|
||||
saveResponse(res)
|
||||
return@tryWithSuspend res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun getSavedToken(context: FragmentActivity): Boolean {
|
||||
return tryWithSuspend(false) {
|
||||
var res: ResponseToken = loadData(MAL_TOKEN, context)
|
||||
?: return@tryWithSuspend false
|
||||
if (System.currentTimeMillis() > res.expiresIn)
|
||||
res = refreshToken()
|
||||
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
|
||||
token = res.accessToken
|
||||
return@tryWithSuspend true
|
||||
} ?: false
|
||||
}
|
||||
|
||||
fun removeSavedToken(context: Context) {
|
||||
token = null
|
||||
username = null
|
||||
userid = null
|
||||
avatar = null
|
||||
if (MAL_TOKEN in context.fileList()) {
|
||||
File(context.filesDir, MAL_TOKEN).delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun saveResponse(res: ResponseToken) {
|
||||
res.expiresIn += System.currentTimeMillis()
|
||||
saveData(MAL_TOKEN, res)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ResponseToken(
|
||||
@SerialName("token_type") val tokenType: String,
|
||||
@SerialName("expires_in") var expiresIn: Long,
|
||||
@SerialName("access_token") val accessToken: String,
|
||||
@SerialName("refresh_token") val refreshToken: String,
|
||||
): java.io.Serializable
|
||||
|
||||
}
|
90
app/src/main/java/ani/dantotsu/connections/mal/MALQueries.kt
Normal file
90
app/src/main/java/ani/dantotsu/connections/mal/MALQueries.kt
Normal file
|
@ -0,0 +1,90 @@
|
|||
package ani.dantotsu.connections.mal
|
||||
|
||||
import ani.dantotsu.connections.anilist.api.FuzzyDate
|
||||
import ani.dantotsu.client
|
||||
import ani.dantotsu.tryWithSuspend
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
class MALQueries {
|
||||
private val apiUrl = "https://api.myanimelist.net/v2"
|
||||
private val authHeader: Map<String, String>?
|
||||
get() {
|
||||
return mapOf("Authorization" to "Bearer ${MAL.token ?: return null}")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class MalUser(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val picture: String?,
|
||||
)
|
||||
|
||||
suspend fun getUserData(): Boolean {
|
||||
val res = tryWithSuspend {
|
||||
client.get(
|
||||
"$apiUrl/users/@me",
|
||||
authHeader ?: return@tryWithSuspend null
|
||||
).parsed<MalUser>()
|
||||
} ?: return false
|
||||
MAL.userid = res.id
|
||||
MAL.username = res.name
|
||||
MAL.avatar = res.picture
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
suspend fun editList(
|
||||
idMAL: Int?,
|
||||
isAnime: Boolean,
|
||||
progress: Int?,
|
||||
score: Int?,
|
||||
status: String,
|
||||
rewatch: Int? = null,
|
||||
start: FuzzyDate? = null,
|
||||
end: FuzzyDate? = null
|
||||
) {
|
||||
if(idMAL==null) return
|
||||
val data = mutableMapOf("status" to convertStatus(isAnime, status))
|
||||
if (progress != null)
|
||||
data[if (isAnime) "num_watched_episodes" else "num_chapters_read"] = progress.toString()
|
||||
data[if (isAnime) "is_rewatching" else "is_rereading"] = (status == "REPEATING").toString()
|
||||
if (score != null)
|
||||
data["score"] = score.div(10).toString()
|
||||
if(rewatch!=null)
|
||||
data[if(isAnime) "num_times_rewatched" else "num_times_reread"] = rewatch.toString()
|
||||
if(start!=null)
|
||||
data["start_date"] = start.toMALString()
|
||||
if(end!=null)
|
||||
data["finish_date"] = end.toMALString()
|
||||
tryWithSuspend {
|
||||
client.put(
|
||||
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
||||
authHeader ?: return@tryWithSuspend null,
|
||||
data = data,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteList(isAnime: Boolean, idMAL: Int?){
|
||||
if(idMAL==null) return
|
||||
tryWithSuspend {
|
||||
client.delete(
|
||||
"$apiUrl/${if (isAnime) "anime" else "manga"}/$idMAL/my_list_status",
|
||||
authHeader ?: return@tryWithSuspend null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertStatus(isAnime: Boolean, status: String): String {
|
||||
return when (status) {
|
||||
"PLANNING" -> if (isAnime) "plan_to_watch" else "plan_to_read"
|
||||
"COMPLETED" -> "completed"
|
||||
"PAUSED" -> "on_hold"
|
||||
"DROPPED" -> "dropped"
|
||||
"CURRENT" -> if (isAnime) "watching" else "reading"
|
||||
else -> if (isAnime) "watching" else "reading"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue