Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,7 @@ class AniListApi : SyncAPI() {

val filtered =
shows?.data?.page?.media?.filter {
(((it.startDate.year ?: year.toString()) == year.toString()
|| year == null))
year == null || (it.startDate?.year ?: year) == year
}
filtered?.forEach {
it.title.romaji?.let { romaji ->
Expand Down Expand Up @@ -547,16 +546,16 @@ class AniListApi : SyncAPI() {

@Serializable
data class CompletedAt(
@JsonProperty("year") @SerialName("year") val year: Int,
@JsonProperty("month") @SerialName("month") val month: Int,
@JsonProperty("day") @SerialName("day") val day: Int,
@JsonProperty("year") @SerialName("year") val year: Int?,
@JsonProperty("month") @SerialName("month") val month: Int?,
@JsonProperty("day") @SerialName("day") val day: Int?,
)

@Serializable
data class StartedAt(
@JsonProperty("year") @SerialName("year") val year: String?,
@JsonProperty("month") @SerialName("month") val month: String?,
@JsonProperty("day") @SerialName("day") val day: String?,
@JsonProperty("year") @SerialName("year") val year: Int?,
@JsonProperty("month") @SerialName("month") val month: Int?,
@JsonProperty("day") @SerialName("day") val day: Int?,
)

@Serializable
Expand All @@ -577,9 +576,9 @@ class AniListApi : SyncAPI() {
@JsonProperty("id") @SerialName("id") val id: Int,
@JsonProperty("idMal") @SerialName("idMal") val idMal: Int?,
@JsonProperty("season") @SerialName("season") val season: String?,
@JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int,
@JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int?,
@JsonProperty("format") @SerialName("format") val format: String?,
@JsonProperty("episodes") @SerialName("episodes") val episodes: Int,
@JsonProperty("episodes") @SerialName("episodes") val episodes: Int?,
@JsonProperty("title") @SerialName("title") val title: Title,
@JsonProperty("description") @SerialName("description") val description: String?,
@JsonProperty("coverImage") @SerialName("coverImage") val coverImage: CoverImage,
Expand All @@ -590,9 +589,9 @@ class AniListApi : SyncAPI() {
@Serializable
data class Entries(
@JsonProperty("status") @SerialName("status") val status: String?,
@JsonProperty("completedAt") @SerialName("completedAt") val completedAt: CompletedAt,
@JsonProperty("startedAt") @SerialName("startedAt") val startedAt: StartedAt,
@JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: Int,
@JsonProperty("completedAt") @SerialName("completedAt") val completedAt: CompletedAt?,
@JsonProperty("startedAt") @SerialName("startedAt") val startedAt: StartedAt?,
@JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: Int?,
@JsonProperty("progress") @SerialName("progress") val progress: Int,
@JsonProperty("score") @SerialName("score") val score: Int,
@JsonProperty("private") @SerialName("private") val private: Boolean,
Expand All @@ -609,14 +608,14 @@ class AniListApi : SyncAPI() {
this.progress,
this.media.episodes,
Score.from100(this.score),
this.updatedAt.toLong(),
this.updatedAt?.toLong(),
"AniList",
TvType.Anime,
this.media.coverImage.extraLarge ?: this.media.coverImage.large
?: this.media.coverImage.medium,
null,
null,
this.media.seasonYear.toYear(),
this.media.seasonYear?.toYear(),
null,
plot = this.media.description,
)
Expand Down Expand Up @@ -792,13 +791,23 @@ class AniListApi : SyncAPI() {
}
"""
} else {
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
aniListStatusString[maxOf(
0,
type.value
)]
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score.toInt(100)}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
// Only declare and pass the optional variables that have a value,
// AniList rejects the whole mutation if an undeclared variable is used
val variables = mutableListOf(
"${'$'}id: Int = $id",
"${'$'}status: MediaListStatus = ${aniListStatusString[maxOf(0, type.value)]}",
)
val arguments = mutableListOf("mediaId: ${'$'}id", "status: ${'$'}status")
if (score != null) {
variables += "${'$'}scoreRaw: Int = ${score.toInt(100)}"
arguments += "scoreRaw: ${'$'}scoreRaw"
}
if (progress != null) {
variables += "${'$'}progress: Int = $progress"
arguments += "progress: ${'$'}progress"
}
"""mutation (${variables.joinToString()}) {
SaveMediaListEntry (${arguments.joinToString()}) {
id
status
progress
Expand All @@ -808,9 +817,25 @@ class AniListApi : SyncAPI() {
}

val data = postApi(auth.token, q)
return data != ""
if (data.isNullOrBlank()) return false
val errors = tryParseJson<GraphQLErrorRoot>(data)?.errors
if (!errors.isNullOrEmpty()) {
logError(ErrorLoadingException("AniList: ${errors.joinToString { it.message.toString() }}"))
return false
}
return true
}

@Serializable
data class GraphQLError(
@JsonProperty("message") @SerialName("message") val message: String?,
)

@Serializable
data class GraphQLErrorRoot(
@JsonProperty("errors") @SerialName("errors") val errors: List<GraphQLError>?,
)

private suspend fun getUser(token: AuthToken): AniListUser? {
val q = """
{
Expand Down Expand Up @@ -1187,9 +1212,9 @@ class AniListApi : SyncAPI() {
data class GetSearchMedia(
@JsonProperty("id") @SerialName("id") val id: Int,
@JsonProperty("idMal") @SerialName("idMal") val idMal: Int?,
@JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int,
@JsonProperty("seasonYear") @SerialName("seasonYear") val seasonYear: Int?,
@JsonProperty("title") @SerialName("title") val title: GetSearchTitle,
@JsonProperty("startDate") @SerialName("startDate") val startDate: StartedAt,
@JsonProperty("startDate") @SerialName("startDate") val startDate: StartedAt?,
@JsonProperty("averageScore") @SerialName("averageScore") val averageScore: Int?,
@JsonProperty("meanScore") @SerialName("meanScore") val meanScore: Int?,
@JsonProperty("bannerImage") @SerialName("bannerImage") val bannerImage: String?,
Expand Down
67 changes: 67 additions & 0 deletions app/src/test/java/com/lagradost/cloudstream3/AniListParsingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.lagradost.cloudstream3

import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/** Ensure AniList responses containing nulls can be parsed. */
class AniListParsingTest {
private val listResponse = """
{"data":{"MediaListCollection":{"lists":[
{"status":"PLANNING","entries":[{
"status":"PLANNING",
"completedAt":{"year":null,"month":null,"day":null},
"startedAt":{"year":null,"month":null,"day":null},
"updatedAt":1761990000,"progress":0,"score":0,"private":false,
"media":{"id":5114,"idMal":5114,"season":null,"seasonYear":null,"format":"TV",
"episodes":null,"chapters":null,
"title":{"english":null,"romaji":"Hagane no Renkinjutsushi: FULLMETAL ALCHEMIST"},
"coverImage":{"extraLarge":null,"large":"https://example.com/l.jpg","medium":null},
"synonyms":[],"nextAiringEpisode":null}
}]},
{"status":"COMPLETED","entries":[{
"status":"COMPLETED",
"completedAt":{"year":2024,"month":3,"day":22},
"startedAt":{"year":2023,"month":9,"day":29},
"updatedAt":1711100000,"progress":28,"score":95,"private":false,
"media":{"id":154587,"idMal":52991,"season":"FALL","seasonYear":2023,"format":"TV",
"episodes":28,"chapters":null,
"title":{"english":"Frieren: Beyond Journey's End","romaji":"Sousou no Frieren"},
"coverImage":{"extraLarge":"https://example.com/xl.jpg","large":null,"medium":null},
"synonyms":["Frieren"],"nextAiringEpisode":null}
}]}
]}}}
""".trimIndent()

@Test
fun `parses library list with null dates and episodes`() {
val lists = parseJson<AniListApi.FullAnilistList>(listResponse)
.data!!.mediaListCollection.lists
assertEquals(2, lists.size)

val planning = lists[0].entries[0]
assertNull(planning.completedAt?.year)
assertNull(planning.media.episodes)
val planningItem = planning.toLibraryItem()
assertEquals("Hagane no Renkinjutsushi: FULLMETAL ALCHEMIST", planningItem.name)
assertNull(planningItem.episodesTotal)

val completed = lists[1].entries[0]
assertEquals(2024, completed.completedAt?.year)
assertEquals(2023, completed.startedAt?.year)
assertEquals("Frieren: Beyond Journey's End", completed.toLibraryItem().name)
}

@Test
fun `detects GraphQL errors in mutation response`() {
val error = """{"errors":[{"message":"Variable \"${'$'}scoreRaw\" is not defined.","status":400}],"data":null}"""
val errors = parseJson<AniListApi.GraphQLErrorRoot>(error).errors
assertEquals("Variable \"\$scoreRaw\" is not defined.", errors?.single()?.message)

val success = """{"data":{"SaveMediaListEntry":{"id":1,"status":"PLANNING","progress":0,"score":0}}}"""
assertTrue(parseJson<AniListApi.GraphQLErrorRoot>(success).errors.isNullOrEmpty())
}
}
Loading