From ec614bb4c3b7d5fe48824d4910adf274e5a6149e Mon Sep 17 00:00:00 2001 From: MajorTanya <39014446+MajorTanya@users.noreply.github.com> Date: Mon, 28 Sep 2026 06:34:26 +0200 Subject: [PATCH] Use Apollo GraphQL for Trackers with GraphQL APIs (#3827) * Add Apollo for GraphQL interactions * Migrate AniList to use Apollo * Migrate Shikimori to use Apollo Not all Shikimori requests can be served by their GraphQL API (notably they do not offer any mutations), so those remain as-is. * Migrate Suwayomi to use Apollo Includes leveraging some slightly newer Suwayomi features, specifically the Double/Float filter fixes first pre-released in 2.1.1985 on 2025-10-26 and first fully released in 2.1.2100 on 2026-05-08. Considering the tachiyomix 1.6 extension lib changes were added in Suwayomi 2.3.2223 on 2026-06-30, I think it is fair to depend on such sorta-recent behaviour. * Fix temp migration names and leftovers * Create ApolloResponse.dataOrElse extension Because Apollo doesn't throw exceptions and instead just provides them as response attributes, the current ApolloResponse.awaitSuccess() is useless and will be removed shortly. This new extension function should wrap the basic handling of an ApolloResponse object neatly. Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com> * Use dataOrElse in AnilistApi.kt * Use dataOrElse in ShikimoriApi.kt * Use dataOrElse in SuwayomiApi.kt * Delete obsolete ApolloCall.awaitSuccess * Migrate Kitsu to use Apollo Kitsu has some unfortunate edges in its GraphQL API (such as returning nonstandard bodies which don't play well with Apollo) but this should be an equivalent implementation. * Extract a MangaFragment for AniList This simplifies the queries and the converters (which can now delegate to the MangaFragment.toTrackSearch() method internally and adjust the track as needed). Also move the toALUser converter to AnilistUtils.kt to keep all converters together. Added a scalar-mapper for AL's CountryCode type. * Minor consistency cleanup of the queries * Store list entry ID not title ID for Shikimori * Extract common MangaFragment for Shikimori Also map the ISO8601Date to a Kotlin String in the service config. * Map Kitsu's ISO8601DateTime to kotlin.time.Instant Utilises Apollo's own adapter-core library which provides the mapper. Requires opting in to ExperimentalTime until the library resolves the outdated annotation. Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com> * Replace Kitsu TODO comment So much for STOPSHIP preventing me shipping this... * Bump Apollo version to 5.1.0 * Bump apollo-adapters-core to 0.8.0 Freshly released just earlier. This resolves having to opt in to ExperimentalTime. * Make new utils internal to align with #3908 * Fix leftover issues from merged #3828 This PR restructured the AniList and Kitsu user objects. * Bump apollo to 5.2.0 * Remove unused attributes from queries * Allow custom exception handler in dataOrElse Enables callers to manually handle specific non-200 responses. Callers MUST close the `ApolloHttpException.body` attached to the passed exception to avoid resource and socket leaks. Defaults to current behaviour of (re)throwing the status code with our HttpException. Utilized in KitsuApi.kt to specially handle Kitsu sending a 500 error response when deleting library IDs not actually in a user's library and falling back to the "throw as HttpException" if it's any other error response. * Rethrow networking/IO exceptions This helps to avoid masking networking problems as "This isn't in your list" problems. Currently, the app shows "Failed to connect to ..." when the remote server is unreachable (e.g. downtime, DNS block, etc) because that's the message provided by the ConnectException thrown by Android's network stack. This change restores that behaviour, approximately. Not the most elegant solution but it _should_ avoid false-positive toasts. --------- Co-authored-by: Mitchell Syer <6090655+Syer10@users.noreply.github.com> --- app/build.gradle.kts | 75 + .../graphql/anilist/AniListAddManga.graphql | 15 + .../anilist/AniListDeleteManga.graphql | 9 + .../anilist/AniListGetCurrentUser.graphql | 9 + .../anilist/AniListGetLibManga.graphql | 31 + .../anilist/AniListGetMangaDetails.graphql | 13 + .../anilist/AniListSearchManga.graphql | 13 + .../anilist/AniListUpdateManga.graphql | 21 + .../graphql/anilist/MangaFragment.graphql | 30 + .../mihon/graphql/anilist/anilist.graphqls | 6319 ++++++++++++++++ .../graphql/kitsu/KitsuAddLibManga.graphql | 27 + .../graphql/kitsu/KitsuDeleteLibEntry.graphql | 18 + .../graphql/kitsu/KitsuFindLibManga.graphql | 18 + .../kitsu/KitsuGetCurrentAccount.graphql | 9 + .../kitsu/KitsuGetMangaDetailsById.graphql | 9 + .../kitsu/KitsuGetMangaDetailsBySlug.graphql | 9 + .../kitsu/KitsuSearchMangaByTitle.graphql | 12 + .../graphql/kitsu/KitsuUpdateLibManga.graphql | 30 + .../mihon/graphql/kitsu/MangaFragment.graphql | 31 + .../mihon/graphql/kitsu/kitsu.graphqls | 6617 +++++++++++++++++ .../graphql/shikimori/MangaFragment.graphql | 22 + .../shikimori/ShikimoriGetCurrentUser.graphql | 6 + .../shikimori/ShikimoriGetLibManga.graphql | 19 + .../ShikimoriGetMangaDetails.graphql | 11 + .../shikimori/ShikimoriSearchManga.graphql | 11 + .../graphql/shikimori/shikimori.graphqls | 1913 +++++ .../graphql/suwayomi/MangaFragment.graphql | 15 + .../graphql/suwayomi/SuwayomiGetManga.graphql | 9 + .../SuwayomiGetMangaUnreadChapters.graphql | 30 + .../suwayomi/SuwayomiMarkChaptersRead.graphql | 14 + .../SuwayomiMarkReadAndDeleteChapters.graphql | 22 + .../SuwayomiUpdateMangaProgress.graphql | 11 + .../mihon/graphql/suwayomi/suwayomi.graphqls | 4883 ++++++++++++ .../tachiyomi/data/track/anilist/Anilist.kt | 6 +- .../data/track/anilist/AnilistApi.kt | 493 +- .../data/track/anilist/AnilistUtils.kt | 129 +- .../data/track/anilist/dto/ALAddManga.kt | 20 - .../data/track/anilist/dto/ALFuzzyDate.kt | 21 - .../data/track/anilist/dto/ALManga.kt | 81 - .../data/track/anilist/dto/ALSearch.kt | 20 - .../data/track/anilist/dto/ALSearchItem.kt | 77 - .../data/track/anilist/dto/ALUser.kt | 19 +- .../data/track/anilist/dto/ALUserList.kt | 45 - .../tachiyomi/data/track/kitsu/Kitsu.kt | 4 +- .../tachiyomi/data/track/kitsu/KitsuApi.kt | 553 +- .../tachiyomi/data/track/kitsu/KitsuUtils.kt | 103 +- .../data/track/kitsu/dto/KitsuAddManga.kt | 23 - .../data/track/kitsu/dto/KitsuCommon.kt | 18 - .../data/track/kitsu/dto/KitsuDeleteManga.kt | 23 - .../data/track/kitsu/dto/KitsuManga.kt | 85 - .../kitsu/dto/KitsuMangaWithLibraryEntry.kt | 74 - .../data/track/kitsu/dto/KitsuSearchById.kt | 25 - .../data/track/kitsu/dto/KitsuSearchBySlug.kt | 13 - .../track/kitsu/dto/KitsuSearchByTitle.kt | 18 - .../data/track/kitsu/dto/KitsuUpdateManga.kt | 23 - .../data/track/kitsu/dto/KitsuUser.kt | 22 +- .../data/track/shikimori/ShikimoriApi.kt | 220 +- .../data/track/shikimori/ShikimoriUtils.kt | 106 +- .../data/track/shikimori/dto/SMManga.kt | 73 - .../data/track/shikimori/dto/SMUser.kt | 13 - .../track/shikimori/dto/SMUserListEntry.kt | 52 - .../tachiyomi/data/track/suwayomi/Suwayomi.kt | 22 +- .../data/track/suwayomi/SuwayomiApi.kt | 253 +- .../SuwayomiMangaFragmentExtensions.kt | 23 + .../data/track/suwayomi/SuwayomiModels.kt | 88 - .../graphql/kitsu/adapter/KitsuMapAdapter.kt | 26 + core/common/build.gradle.kts | 2 + .../tachiyomi/network/ApolloExtensions.kt | 56 + gradle/libs.versions.toml | 5 + 69 files changed, 21163 insertions(+), 1952 deletions(-) create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListAddManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListDeleteManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListGetCurrentUser.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListGetLibManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListGetMangaDetails.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListSearchManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/AniListUpdateManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/MangaFragment.graphql create mode 100644 app/src/main/graphql/mihon/graphql/anilist/anilist.graphqls create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuAddLibManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuDeleteLibEntry.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuFindLibManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuGetCurrentAccount.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsById.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsBySlug.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuSearchMangaByTitle.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/KitsuUpdateLibManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/MangaFragment.graphql create mode 100644 app/src/main/graphql/mihon/graphql/kitsu/kitsu.graphqls create mode 100644 app/src/main/graphql/mihon/graphql/shikimori/MangaFragment.graphql create mode 100644 app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetCurrentUser.graphql create mode 100644 app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetLibManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetMangaDetails.graphql create mode 100644 app/src/main/graphql/mihon/graphql/shikimori/ShikimoriSearchManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/shikimori/shikimori.graphqls create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/MangaFragment.graphql create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetManga.graphql create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetMangaUnreadChapters.graphql create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkChaptersRead.graphql create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkReadAndDeleteChapters.graphql create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiUpdateMangaProgress.graphql create mode 100644 app/src/main/graphql/mihon/graphql/suwayomi/suwayomi.graphqls delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuCommon.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuDeleteManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaWithLibraryEntry.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchById.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchBySlug.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchByTitle.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUpdateManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiMangaFragmentExtensions.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt create mode 100644 app/src/main/java/mihon/graphql/kitsu/adapter/KitsuMapAdapter.kt create mode 100644 core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ApolloExtensions.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 607ae02db..3b59700b7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(libs.plugins.aboutLibraries) alias(libs.plugins.androidx.baselineProfile) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.apollo) } if (Config.includeTelemetry) { @@ -318,6 +319,10 @@ dependencies { // String similarity implementation(libs.stringSimilarity) + // GraphQL generation + implementation(libs.apollo) + implementation(libs.apollo.adapters) + // Tests testImplementation(libs.bundles.test) testRuntimeOnly(libs.junit.platform.launcher) @@ -329,6 +334,76 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) } +apollo { + val schemaBasePath = "src/main/graphql/mihon/graphql" + service("anilist") { + packageName.set("mihon.graphql.anilist") + val srcDir = "$schemaBasePath/anilist" + srcDir(file(srcDir)) + + introspection { + endpointUrl.set("https://graphql.anilist.co") + schemaFile.set(file("$srcDir/anilist.graphqls")) + } + + // ISO 3166-1 alpha-2 country code + mapScalarToKotlinString("CountryCode") + } + + service("kitsu") { + packageName.set("mihon.graphql.kitsu") + val srcDir = "$schemaBasePath/kitsu" + srcDir(file(srcDir)) + android.defaultConfig.versionCode + + introspection { + // Kitsu doesn't like requests without a UA + headers.put( + "User-Agent", + "Mihon v${android.defaultConfig.versionName} (${android.defaultConfig.applicationId})", + ) + endpointUrl.set("https://kitsu.app/api/graphql") + schemaFile.set(file("$srcDir/kitsu.graphqls")) + } + + // A date, expressed as an ISO8601 string + mapScalarToKotlinString("Date") + // An ISO 8601-encoded datetime + mapScalar("ISO8601DateTime", "kotlin.time.Instant", "com.apollographql.adapter.core.KotlinInstantAdapter") + // A loose key-value map in GraphQL + mapScalar( + "Map", + "kotlin.collections.Map", + "mihon.graphql.kitsu.adapter.KitsuMapAdapter", + ) + } + + service("shikimori") { + packageName.set("mihon.graphql.shikimori") + val srcDir = "$schemaBasePath/shikimori" + srcDir(file(srcDir)) + + introspection { + endpointUrl.set("https://shikimori.io/api/graphql") + schemaFile.set(file("$srcDir/shikimori.graphqls")) + } + + // An ISO 8601-encoded date + mapScalarToKotlinString("ISO8601Date") + } + + service("suwayomi") { + packageName.set("mihon.graphql.suwayomi") + val srcDir = "$schemaBasePath/suwayomi" + srcDir(file(srcDir)) + + introspection { + endpointUrl.set("http://localhost:4567/api/graphql") + schemaFile.set(file("$srcDir/suwayomi.graphqls")) + } + } +} + val latestCommitCount = getLatestCommitCount() val latestCommitSha = getLatestCommitSha() val latestCommitTime = getLatestCommitTime() diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListAddManga.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListAddManga.graphql new file mode 100644 index 000000000..61d2ab0a4 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListAddManga.graphql @@ -0,0 +1,15 @@ +mutation AniListAddManga( + $manga_id: Int!, + $progress: Int!, + $status: MediaListStatus!, + $private: Boolean!, +) { + SaveMediaListEntry ( + mediaId: $manga_id, + progress: $progress, + status: $status, + private: $private, + ) { + id + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListDeleteManga.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListDeleteManga.graphql new file mode 100644 index 000000000..4bc911491 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListDeleteManga.graphql @@ -0,0 +1,9 @@ +mutation AniListDeleteManga( + $library_id: Int!, +) { + DeleteMediaListEntry( + id: $library_id, + ) { + deleted + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListGetCurrentUser.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListGetCurrentUser.graphql new file mode 100644 index 000000000..085e58892 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListGetCurrentUser.graphql @@ -0,0 +1,9 @@ +query AniListGetCurrentUser { + Viewer { + id + name + mediaListOptions { + scoreFormat + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListGetLibManga.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListGetLibManga.graphql new file mode 100644 index 000000000..02759b222 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListGetLibManga.graphql @@ -0,0 +1,31 @@ +query AniListGetLibManga( + $user_id: Int!, + $manga_id: Int!, +) { + Page { + mediaList( + userId: $user_id, + type: MANGA, + mediaId: $manga_id, + ) { + id + status + scoreRaw: score(format: POINT_100) + progress + private + startedAt { + year + month + day + } + completedAt { + year + month + day + } + media { + ...MangaFragment + } + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListGetMangaDetails.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListGetMangaDetails.graphql new file mode 100644 index 000000000..0ac932d02 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListGetMangaDetails.graphql @@ -0,0 +1,13 @@ +query AniListGetMangaDetails( + $manga_id: Int!, +) { + Page (perPage: 1) { + media( + id: $manga_id, + type: MANGA, + format_not_in: [NOVEL], + ) { + ...MangaFragment + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListSearchManga.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListSearchManga.graphql new file mode 100644 index 000000000..8093396d2 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListSearchManga.graphql @@ -0,0 +1,13 @@ +query AniListSearchManga( + $search: String!, +) { + Page (perPage: 50) { + media( + search: $search, + type: MANGA, + format_not_in: [NOVEL], + ) { + ...MangaFragment + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/AniListUpdateManga.graphql b/app/src/main/graphql/mihon/graphql/anilist/AniListUpdateManga.graphql new file mode 100644 index 000000000..003efe551 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/AniListUpdateManga.graphql @@ -0,0 +1,21 @@ +mutation AniListUpdateManga( + $library_id: Int!, + $progress: Int!, + $status: MediaListStatus!, + $private: Boolean!, + $score: Int!, + $startedAt: FuzzyDateInput!, + $completedAt: FuzzyDateInput!, +) { + SaveMediaListEntry( + id: $library_id, + progress: $progress, + status: $status, + private: $private, + scoreRaw: $score, + startedAt: $startedAt, + completedAt: $completedAt, + ) { + id + } +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/MangaFragment.graphql b/app/src/main/graphql/mihon/graphql/anilist/MangaFragment.graphql new file mode 100644 index 000000000..f86d0deb7 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/MangaFragment.graphql @@ -0,0 +1,30 @@ +fragment MangaFragment on Media { + id + staff { + edges { + role + node { + name { + userPreferred + } + } + } + } + title { + userPreferred + } + coverImage { + large + } + format + countryOfOrigin + status + chapters + description + startDate { + year + month + day + } + averageScore +} diff --git a/app/src/main/graphql/mihon/graphql/anilist/anilist.graphqls b/app/src/main/graphql/mihon/graphql/anilist/anilist.graphqls new file mode 100644 index 000000000..9efc13585 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/anilist/anilist.graphqls @@ -0,0 +1,6319 @@ +type Query { + Page("The page number" page: Int, "The amount of entries per page, max 50" perPage: Int): Page + + """ + Media query + """ + Media("Filter by the media id" id: Int, "Filter by the media's MyAnimeList id" idMal: Int, "Filter by the start date of the media" startDate: FuzzyDateInt, "Filter by the end date of the media" endDate: FuzzyDateInt, "Filter by the season the media was released in" season: MediaSeason, "The year of the season (Winter 2017 would also include December 2016 releases). Requires season argument" seasonYear: Int, "Filter by the media's type" type: MediaType, "Filter by the media's format" format: MediaFormat, "Filter by the media's current release status" status: MediaStatus, "Filter by amount of episodes the media has" episodes: Int, "Filter by the media's episode length" duration: Int, "Filter by the media's chapter count" chapters: Int, "Filter by the media's volume count" volumes: Int, "Filter by if the media's intended for 18+ adult audiences" isAdult: Boolean, "Filter by the media's genres" genre: String, "Filter by the media's tags" tag: String, "Only apply the tags filter argument to tags above this rank. Default: 18" minimumTagRank: Int, "Filter by the media's tags with in a tag category" tagCategory: String, "Filter by the media on the authenticated user's lists" onList: Boolean, "Filter media by sites name with a online streaming or reading license" licensedBy: String, "Filter media by sites id with a online streaming or reading license" licensedById: Int, "Filter by the media's average score" averageScore: Int, "Filter by the number of users with this media on their list" popularity: Int, "Filter by the source type of the media" source: MediaSource, "Filter by the media's country of origin" countryOfOrigin: CountryCode, "If the media is officially licensed or a self-published doujin release" isLicensed: Boolean, "Filter by search query" search: String, "Filter by the media id" id_not: Int, "Filter by the media id (max 10,000 items)" id_in: [Int], "Filter by the media id (max 10,000 items)" id_not_in: [Int], "Filter by the media's MyAnimeList id" idMal_not: Int, "Filter by the media's MyAnimeList id (max 10,000 items)" idMal_in: [Int], "Filter by the media's MyAnimeList id (max 10,000 items)" idMal_not_in: [Int], "Filter by the start date of the media" startDate_greater: FuzzyDateInt, "Filter by the start date of the media" startDate_lesser: FuzzyDateInt, "Filter by the start date of the media" startDate_like: String, "Filter by the end date of the media" endDate_greater: FuzzyDateInt, "Filter by the end date of the media" endDate_lesser: FuzzyDateInt, "Filter by the end date of the media" endDate_like: String, "Filter by the media's format (max 10,000 items)" format_in: [MediaFormat], "Filter by the media's format" format_not: MediaFormat, "Filter by the media's format (max 10,000 items)" format_not_in: [MediaFormat], "Filter by the media's current release status (max 10,000 items)" status_in: [MediaStatus], "Filter by the media's current release status" status_not: MediaStatus, "Filter by the media's current release status (max 10,000 items)" status_not_in: [MediaStatus], "Filter by amount of episodes the media has" episodes_greater: Int, "Filter by amount of episodes the media has" episodes_lesser: Int, "Filter by the media's episode length" duration_greater: Int, "Filter by the media's episode length" duration_lesser: Int, "Filter by the media's chapter count" chapters_greater: Int, "Filter by the media's chapter count" chapters_lesser: Int, "Filter by the media's volume count" volumes_greater: Int, "Filter by the media's volume count" volumes_lesser: Int, "Filter by the media's genres (max 10,000 items)" genre_in: [String], "Filter by the media's genres (max 10,000 items)" genre_not_in: [String], "Filter by the media's tags (max 10,000 items)" tag_in: [String], "Filter by the media's tags (max 10,000 items)" tag_not_in: [String], "Filter by the media's tags with in a tag category (max 10,000 items)" tagCategory_in: [String], "Filter by the media's tags with in a tag category (max 10,000 items)" tagCategory_not_in: [String], "Filter media by sites name with a online streaming or reading license (max 10,000 items)" licensedBy_in: [String], "Filter media by sites id with a online streaming or reading license (max 10,000 items)" licensedById_in: [Int], "Filter by the media's average score" averageScore_not: Int, "Filter by the media's average score" averageScore_greater: Int, "Filter by the media's average score" averageScore_lesser: Int, "Filter by the number of users with this media on their list" popularity_not: Int, "Filter by the number of users with this media on their list" popularity_greater: Int, "Filter by the number of users with this media on their list" popularity_lesser: Int, "Filter by the source type of the media (max 10,000 items)" source_in: [MediaSource], "Filter by the media's country of origin (max 10,000 items)" countryOfOrigin_in: [CountryCode], "Filter by the media's country of origin (max 10,000 items)" countryOfOrigin_not_in: [CountryCode], "The order the results will be returned in" sort: [MediaSort]): Media + + """ + Media Trend query + """ + MediaTrend("Filter by the media id" mediaId: Int, "Filter by date" date: Int, "Filter by trending amount" trending: Int, "Filter by score" averageScore: Int, "Filter by popularity" popularity: Int, "Filter by episode number" episode: Int, "Filter to stats recorded while the media was releasing" releasing: Boolean, "Filter by the media id" mediaId_not: Int, "Filter by the media id (max 10,000 items)" mediaId_in: [Int], "Filter by the media id (max 10,000 items)" mediaId_not_in: [Int], "Filter by date" date_greater: Int, "Filter by date" date_lesser: Int, "Filter by trending amount" trending_greater: Int, "Filter by trending amount" trending_lesser: Int, "Filter by trending amount" trending_not: Int, "Filter by score" averageScore_greater: Int, "Filter by score" averageScore_lesser: Int, "Filter by score" averageScore_not: Int, "Filter by popularity" popularity_greater: Int, "Filter by popularity" popularity_lesser: Int, "Filter by popularity" popularity_not: Int, "Filter by episode number" episode_greater: Int, "Filter by episode number" episode_lesser: Int, "Filter by episode number" episode_not: Int, "The order the results will be returned in" sort: [MediaTrendSort]): MediaTrend + + """ + Airing schedule query + """ + AiringSchedule("Filter by the id of the airing schedule item" id: Int, "Filter by the id of associated media" mediaId: Int, "Filter by the airing episode number" episode: Int, "Filter by the time of airing" airingAt: Int, "Filter to episodes that haven't yet aired" notYetAired: Boolean, "Filter by the id of the airing schedule item" id_not: Int, "Filter by the id of the airing schedule item (max 10,000 items)" id_in: [Int], "Filter by the id of the airing schedule item (max 10,000 items)" id_not_in: [Int], "Filter by the id of associated media" mediaId_not: Int, "Filter by the id of associated media (max 10,000 items)" mediaId_in: [Int], "Filter by the id of associated media (max 10,000 items)" mediaId_not_in: [Int], "Filter by the airing episode number" episode_not: Int, "Filter by the airing episode number (max 10,000 items)" episode_in: [Int], "Filter by the airing episode number (max 10,000 items)" episode_not_in: [Int], "Filter by the airing episode number" episode_greater: Int, "Filter by the airing episode number" episode_lesser: Int, "Filter by the time of airing" airingAt_greater: Int, "Filter by the time of airing" airingAt_lesser: Int, "The order the results will be returned in" sort: [AiringSort]): AiringSchedule + + """ + Character query + """ + Character("Filter by character id" id: Int, "Filter by character by if its their birthday today" isBirthday: Boolean, "Filter by search query" search: String, "Filter by character id" id_not: Int, "Filter by character id (max 10,000 items)" id_in: [Int], "Filter by character id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [CharacterSort]): Character + + """ + Staff query + """ + Staff("Filter by the staff id" id: Int, "Filter by staff by if its their birthday today" isBirthday: Boolean, "Filter by search query" search: String, "Filter by the staff id" id_not: Int, "Filter by the staff id (max 10,000 items)" id_in: [Int], "Filter by the staff id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [StaffSort]): Staff + + """ + Media list query + """ + MediaList("Filter by a list entry's id" id: Int, "Filter by a user's id" userId: Int, "Filter by a user's name" userName: String, "Filter by the list entries media type" type: MediaType, "Filter by the watching/reading status" status: MediaListStatus, "Filter by the media id of the list entry" mediaId: Int, "Filter list entries to users who are being followed by the authenticated user" isFollowing: Boolean, "Filter by note words and #tags" notes: String, "Filter by the date the user started the media" startedAt: FuzzyDateInt, "Filter by the date the user completed the media" completedAt: FuzzyDateInt, "Limit to only entries also on the auth user's list. Requires user id or name arguments." compareWithAuthList: Boolean, "Filter by a user's id (max 10,000 items)" userId_in: [Int], "Filter by the watching/reading status (max 10,000 items)" status_in: [MediaListStatus], "Filter by the watching/reading status (max 10,000 items)" status_not_in: [MediaListStatus], "Filter by the watching/reading status" status_not: MediaListStatus, "Filter by the media id of the list entry (max 10,000 items)" mediaId_in: [Int], "Filter by the media id of the list entry (max 10,000 items)" mediaId_not_in: [Int], "Filter by note words and #tags" notes_like: String, "Filter by the date the user started the media" startedAt_greater: FuzzyDateInt, "Filter by the date the user started the media" startedAt_lesser: FuzzyDateInt, "Filter by the date the user started the media" startedAt_like: String, "Filter by the date the user completed the media" completedAt_greater: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_lesser: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_like: String, "The order the results will be returned in" sort: [MediaListSort]): MediaList + + """ + Media list collection query, provides list pre-grouped by status & custom lists. User ID and Media Type arguments required. + """ + MediaListCollection("Filter by a user's id" userId: Int, "Filter by a user's name" userName: String, "Filter by the list entries media type" type: MediaType, "Filter by the watching/reading status" status: MediaListStatus, "Filter by note words and #tags" notes: String, "Filter by the date the user started the media" startedAt: FuzzyDateInt, "Filter by the date the user completed the media" completedAt: FuzzyDateInt, "Always return completed list entries in one group, overriding the user's split completed option." forceSingleCompletedList: Boolean, "Which chunk of list entries to load" chunk: Int, "The amount of entries per chunk, max 500" perChunk: Int, "Filter by the watching/reading status (max 10,000 items)" status_in: [MediaListStatus], "Filter by the watching/reading status (max 10,000 items)" status_not_in: [MediaListStatus], "Filter by the watching/reading status" status_not: MediaListStatus, "Filter by note words and #tags" notes_like: String, "Filter by the date the user started the media" startedAt_greater: FuzzyDateInt, "Filter by the date the user started the media" startedAt_lesser: FuzzyDateInt, "Filter by the date the user started the media" startedAt_like: String, "Filter by the date the user completed the media" completedAt_greater: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_lesser: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_like: String, "The order the results will be returned in" sort: [MediaListSort]): MediaListCollection + + """ + Collection of all the possible media genres + """ + GenreCollection: [String] + + """ + Collection of all the possible media tags + """ + MediaTagCollection("Mod Only" status: Int): [MediaTag] + + """ + User query + """ + User("Filter by the user id" id: Int, "Filter by the name of the user" name: String, "Filter to moderators only if true" isModerator: Boolean, "Filter by search query" search: String, "The order the results will be returned in" sort: [UserSort]): User + + """ + Get the currently authenticated user + """ + Viewer: User + + """ + Notification query + """ + Notification("Filter by the type of notifications" type: NotificationType, "Reset the unread notification count to 0 on load" resetNotificationCount: Boolean, "Filter by the type of notifications (max 10,000 items)" type_in: [NotificationType]): NotificationUnion + + """ + Studio query + """ + Studio("Filter by the studio id" id: Int, "Filter by search query" search: String, "Filter by the studio id" id_not: Int, "Filter by the studio id (max 10,000 items)" id_in: [Int], "Filter by the studio id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [StudioSort]): Studio + + """ + Review query + """ + Review("Filter by Review id" id: Int, "Filter by media id" mediaId: Int, "Filter by user id" userId: Int, "Filter by media type" mediaType: MediaType, "The order the results will be returned in" sort: [ReviewSort]): Review + + """ + Activity query + """ + Activity("Filter by the activity id" id: Int, "Filter by the owner user id" userId: Int, "Filter by the id of the user who sent a message" messengerId: Int, "Filter by the associated media id of the activity" mediaId: Int, "Filter by the type of activity" type: ActivityType, "Filter activity to users who are being followed by the authenticated user" isFollowing: Boolean, "Filter activity to only activity with replies" hasReplies: Boolean, "Filter activity to only activity with replies or is of type text" hasRepliesOrTypeText: Boolean, "Filter by the time the activity was created" createdAt: Int, "Filter by the activity id" id_not: Int, "Filter by the activity id (max 10,000 items)" id_in: [Int], "Filter by the activity id (max 10,000 items)" id_not_in: [Int], "Filter by the owner user id" userId_not: Int, "Filter by the owner user id (max 10,000 items)" userId_in: [Int], "Filter by the owner user id (max 10,000 items)" userId_not_in: [Int], "Filter by the id of the user who sent a message" messengerId_not: Int, "Filter by the id of the user who sent a message (max 10,000 items)" messengerId_in: [Int], "Filter by the id of the user who sent a message (max 10,000 items)" messengerId_not_in: [Int], "Filter by the associated media id of the activity" mediaId_not: Int, "Filter by the associated media id of the activity (max 10,000 items)" mediaId_in: [Int], "Filter by the associated media id of the activity (max 10,000 items)" mediaId_not_in: [Int], "Filter by the type of activity" type_not: ActivityType, "Filter by the type of activity (max 10,000 items)" type_in: [ActivityType], "Filter by the type of activity (max 10,000 items)" type_not_in: [ActivityType], "Filter by the time the activity was created" createdAt_greater: Int, "Filter by the time the activity was created" createdAt_lesser: Int, "The order the results will be returned in" sort: [ActivitySort]): ActivityUnion + + """ + Activity reply query + """ + ActivityReply("Filter by the reply id" id: Int, "Filter by the parent id" activityId: Int): ActivityReply + + """ + Following query + """ + Following("User id of the follower/followed" userId: Int!, "The order the results will be returned in" sort: [UserSort]): User + + """ + Follower query + """ + Follower("User id of the follower/followed" userId: Int!, "The order the results will be returned in" sort: [UserSort]): User + + """ + Thread query + """ + Thread("Filter by the thread id" id: Int, "Filter by the user id of the thread's creator" userId: Int, "Filter by the user id of the last user to comment on the thread" replyUserId: Int, "Filter by if the currently authenticated user's subscribed threads" subscribed: Boolean, "Filter by thread category id" categoryId: Int, "Filter by thread media id category" mediaCategoryId: Int, "Filter by search query" search: String, "Filter by the thread id (max 10,000 items)" id_in: [Int], "The order the results will be returned in" sort: [ThreadSort]): Thread + + """ + Comment query + """ + ThreadComment("Filter by the comment id" id: Int, "Filter by the thread id" threadId: Int, "Filter by the user id of the comment's creator" userId: Int, "The order the results will be returned in" sort: [ThreadCommentSort]): [ThreadComment] + + """ + Recommendation query + """ + Recommendation("Filter by recommendation id" id: Int, "Filter by media id" mediaId: Int, "Filter by media recommendation id" mediaRecommendationId: Int, "Filter by user who created the recommendation" userId: Int, "Filter by total rating of the recommendation" rating: Int, "Filter by the media on the authenticated user's lists" onList: Boolean, "Filter by total rating of the recommendation" rating_greater: Int, "Filter by total rating of the recommendation" rating_lesser: Int, "The order the results will be returned in" sort: [RecommendationSort]): Recommendation + + """ + Like query + """ + Like("The id of the likeable type" likeableId: Int, "The type of model the id applies to" type: LikeableType): User + + """ + Provide AniList markdown to be converted to html (Requires auth) + """ + Markdown("The markdown to be parsed to html" markdown: String!): ParsedMarkdown + + AniChartUser: AniChartUser + + """ + Site statistics query + """ + SiteStatistics: SiteStatistics + + """ + ExternalLinkSource collection query + """ + ExternalLinkSourceCollection("Filter by the link id" id: Int, type: ExternalLinkType, mediaType: ExternalLinkMediaType): [MediaExternalLink] +} + +""" +The `Int` scalar type represents non-fractional signed whole numeric +values. Int can represent values between -(2^31) and 2^31 - 1. +""" +scalar Int + +""" +Page of data. Limited to a max depth of 5000 entries. This is calculated as the page parameter multiplied by the perPage parameter. +""" +type Page { + """ + The pagination information + """ + pageInfo: PageInfo + + users("Filter by the user id" id: Int, "Filter by the name of the user" name: String, "Filter to moderators only if true" isModerator: Boolean, "Filter by search query" search: String, "The order the results will be returned in" sort: [UserSort]): [User] + + media("Filter by the media id" id: Int, "Filter by the media's MyAnimeList id" idMal: Int, "Filter by the start date of the media" startDate: FuzzyDateInt, "Filter by the end date of the media" endDate: FuzzyDateInt, "Filter by the season the media was released in" season: MediaSeason, "The year of the season (Winter 2017 would also include December 2016 releases). Requires season argument" seasonYear: Int, "Filter by the media's type" type: MediaType, "Filter by the media's format" format: MediaFormat, "Filter by the media's current release status" status: MediaStatus, "Filter by amount of episodes the media has" episodes: Int, "Filter by the media's episode length" duration: Int, "Filter by the media's chapter count" chapters: Int, "Filter by the media's volume count" volumes: Int, "Filter by if the media's intended for 18+ adult audiences" isAdult: Boolean, "Filter by the media's genres" genre: String, "Filter by the media's tags" tag: String, "Only apply the tags filter argument to tags above this rank. Default: 18" minimumTagRank: Int, "Filter by the media's tags with in a tag category" tagCategory: String, "Filter by the media on the authenticated user's lists" onList: Boolean, "Filter media by sites name with a online streaming or reading license" licensedBy: String, "Filter media by sites id with a online streaming or reading license" licensedById: Int, "Filter by the media's average score" averageScore: Int, "Filter by the number of users with this media on their list" popularity: Int, "Filter by the source type of the media" source: MediaSource, "Filter by the media's country of origin" countryOfOrigin: CountryCode, "If the media is officially licensed or a self-published doujin release" isLicensed: Boolean, "Filter by search query" search: String, "Filter by the media id" id_not: Int, "Filter by the media id (max 10,000 items)" id_in: [Int], "Filter by the media id (max 10,000 items)" id_not_in: [Int], "Filter by the media's MyAnimeList id" idMal_not: Int, "Filter by the media's MyAnimeList id (max 10,000 items)" idMal_in: [Int], "Filter by the media's MyAnimeList id (max 10,000 items)" idMal_not_in: [Int], "Filter by the start date of the media" startDate_greater: FuzzyDateInt, "Filter by the start date of the media" startDate_lesser: FuzzyDateInt, "Filter by the start date of the media" startDate_like: String, "Filter by the end date of the media" endDate_greater: FuzzyDateInt, "Filter by the end date of the media" endDate_lesser: FuzzyDateInt, "Filter by the end date of the media" endDate_like: String, "Filter by the media's format (max 10,000 items)" format_in: [MediaFormat], "Filter by the media's format" format_not: MediaFormat, "Filter by the media's format (max 10,000 items)" format_not_in: [MediaFormat], "Filter by the media's current release status (max 10,000 items)" status_in: [MediaStatus], "Filter by the media's current release status" status_not: MediaStatus, "Filter by the media's current release status (max 10,000 items)" status_not_in: [MediaStatus], "Filter by amount of episodes the media has" episodes_greater: Int, "Filter by amount of episodes the media has" episodes_lesser: Int, "Filter by the media's episode length" duration_greater: Int, "Filter by the media's episode length" duration_lesser: Int, "Filter by the media's chapter count" chapters_greater: Int, "Filter by the media's chapter count" chapters_lesser: Int, "Filter by the media's volume count" volumes_greater: Int, "Filter by the media's volume count" volumes_lesser: Int, "Filter by the media's genres (max 10,000 items)" genre_in: [String], "Filter by the media's genres (max 10,000 items)" genre_not_in: [String], "Filter by the media's tags (max 10,000 items)" tag_in: [String], "Filter by the media's tags (max 10,000 items)" tag_not_in: [String], "Filter by the media's tags with in a tag category (max 10,000 items)" tagCategory_in: [String], "Filter by the media's tags with in a tag category (max 10,000 items)" tagCategory_not_in: [String], "Filter media by sites name with a online streaming or reading license (max 10,000 items)" licensedBy_in: [String], "Filter media by sites id with a online streaming or reading license (max 10,000 items)" licensedById_in: [Int], "Filter by the media's average score" averageScore_not: Int, "Filter by the media's average score" averageScore_greater: Int, "Filter by the media's average score" averageScore_lesser: Int, "Filter by the number of users with this media on their list" popularity_not: Int, "Filter by the number of users with this media on their list" popularity_greater: Int, "Filter by the number of users with this media on their list" popularity_lesser: Int, "Filter by the source type of the media (max 10,000 items)" source_in: [MediaSource], "Filter by the media's country of origin (max 10,000 items)" countryOfOrigin_in: [CountryCode], "Filter by the media's country of origin (max 10,000 items)" countryOfOrigin_not_in: [CountryCode], "The order the results will be returned in" sort: [MediaSort]): [Media] + + characters("Filter by character id" id: Int, "Filter by character by if its their birthday today" isBirthday: Boolean, "Filter by search query" search: String, "Filter by character id" id_not: Int, "Filter by character id (max 10,000 items)" id_in: [Int], "Filter by character id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [CharacterSort]): [Character] + + staff("Filter by the staff id" id: Int, "Filter by staff by if its their birthday today" isBirthday: Boolean, "Filter by search query" search: String, "Filter by the staff id" id_not: Int, "Filter by the staff id (max 10,000 items)" id_in: [Int], "Filter by the staff id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [StaffSort]): [Staff] + + studios("Filter by the studio id" id: Int, "Filter by search query" search: String, "Filter by the studio id" id_not: Int, "Filter by the studio id (max 10,000 items)" id_in: [Int], "Filter by the studio id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [StudioSort]): [Studio] + + mediaList("Filter by a list entry's id" id: Int, "Filter by a user's id" userId: Int, "Filter by a user's name" userName: String, "Filter by the list entries media type" type: MediaType, "Filter by the watching/reading status" status: MediaListStatus, "Filter by the media id of the list entry" mediaId: Int, "Filter list entries to users who are being followed by the authenticated user" isFollowing: Boolean, "Filter by note words and #tags" notes: String, "Filter by the date the user started the media" startedAt: FuzzyDateInt, "Filter by the date the user completed the media" completedAt: FuzzyDateInt, "Limit to only entries also on the auth user's list. Requires user id or name arguments." compareWithAuthList: Boolean, "Filter by a user's id (max 10,000 items)" userId_in: [Int], "Filter by the watching/reading status (max 10,000 items)" status_in: [MediaListStatus], "Filter by the watching/reading status (max 10,000 items)" status_not_in: [MediaListStatus], "Filter by the watching/reading status" status_not: MediaListStatus, "Filter by the media id of the list entry (max 10,000 items)" mediaId_in: [Int], "Filter by the media id of the list entry (max 10,000 items)" mediaId_not_in: [Int], "Filter by note words and #tags" notes_like: String, "Filter by the date the user started the media" startedAt_greater: FuzzyDateInt, "Filter by the date the user started the media" startedAt_lesser: FuzzyDateInt, "Filter by the date the user started the media" startedAt_like: String, "Filter by the date the user completed the media" completedAt_greater: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_lesser: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_like: String, "The order the results will be returned in" sort: [MediaListSort]): [MediaList] + + airingSchedules("Filter by the id of the airing schedule item" id: Int, "Filter by the id of associated media" mediaId: Int, "Filter by the airing episode number" episode: Int, "Filter by the time of airing" airingAt: Int, "Filter to episodes that haven't yet aired" notYetAired: Boolean, "Filter by the id of the airing schedule item" id_not: Int, "Filter by the id of the airing schedule item (max 10,000 items)" id_in: [Int], "Filter by the id of the airing schedule item (max 10,000 items)" id_not_in: [Int], "Filter by the id of associated media" mediaId_not: Int, "Filter by the id of associated media (max 10,000 items)" mediaId_in: [Int], "Filter by the id of associated media (max 10,000 items)" mediaId_not_in: [Int], "Filter by the airing episode number" episode_not: Int, "Filter by the airing episode number (max 10,000 items)" episode_in: [Int], "Filter by the airing episode number (max 10,000 items)" episode_not_in: [Int], "Filter by the airing episode number" episode_greater: Int, "Filter by the airing episode number" episode_lesser: Int, "Filter by the time of airing" airingAt_greater: Int, "Filter by the time of airing" airingAt_lesser: Int, "The order the results will be returned in" sort: [AiringSort]): [AiringSchedule] + + mediaTrends("Filter by the media id" mediaId: Int, "Filter by date" date: Int, "Filter by trending amount" trending: Int, "Filter by score" averageScore: Int, "Filter by popularity" popularity: Int, "Filter by episode number" episode: Int, "Filter to stats recorded while the media was releasing" releasing: Boolean, "Filter by the media id" mediaId_not: Int, "Filter by the media id (max 10,000 items)" mediaId_in: [Int], "Filter by the media id (max 10,000 items)" mediaId_not_in: [Int], "Filter by date" date_greater: Int, "Filter by date" date_lesser: Int, "Filter by trending amount" trending_greater: Int, "Filter by trending amount" trending_lesser: Int, "Filter by trending amount" trending_not: Int, "Filter by score" averageScore_greater: Int, "Filter by score" averageScore_lesser: Int, "Filter by score" averageScore_not: Int, "Filter by popularity" popularity_greater: Int, "Filter by popularity" popularity_lesser: Int, "Filter by popularity" popularity_not: Int, "Filter by episode number" episode_greater: Int, "Filter by episode number" episode_lesser: Int, "Filter by episode number" episode_not: Int, "The order the results will be returned in" sort: [MediaTrendSort]): [MediaTrend] + + notifications("Filter by the type of notifications" type: NotificationType, "Reset the unread notification count to 0 on load" resetNotificationCount: Boolean, "Filter by the type of notifications (max 10,000 items)" type_in: [NotificationType]): [NotificationUnion] + + followers("User id of the follower/followed" userId: Int!, "The order the results will be returned in" sort: [UserSort]): [User] + + following("User id of the follower/followed" userId: Int!, "The order the results will be returned in" sort: [UserSort]): [User] + + activities("Filter by the activity id" id: Int, "Filter by the owner user id" userId: Int, "Filter by the id of the user who sent a message" messengerId: Int, "Filter by the associated media id of the activity" mediaId: Int, "Filter by the type of activity" type: ActivityType, "Filter activity to users who are being followed by the authenticated user" isFollowing: Boolean, "Filter activity to only activity with replies" hasReplies: Boolean, "Filter activity to only activity with replies or is of type text" hasRepliesOrTypeText: Boolean, "Filter by the time the activity was created" createdAt: Int, "Filter by the activity id" id_not: Int, "Filter by the activity id (max 10,000 items)" id_in: [Int], "Filter by the activity id (max 10,000 items)" id_not_in: [Int], "Filter by the owner user id" userId_not: Int, "Filter by the owner user id (max 10,000 items)" userId_in: [Int], "Filter by the owner user id (max 10,000 items)" userId_not_in: [Int], "Filter by the id of the user who sent a message" messengerId_not: Int, "Filter by the id of the user who sent a message (max 10,000 items)" messengerId_in: [Int], "Filter by the id of the user who sent a message (max 10,000 items)" messengerId_not_in: [Int], "Filter by the associated media id of the activity" mediaId_not: Int, "Filter by the associated media id of the activity (max 10,000 items)" mediaId_in: [Int], "Filter by the associated media id of the activity (max 10,000 items)" mediaId_not_in: [Int], "Filter by the type of activity" type_not: ActivityType, "Filter by the type of activity (max 10,000 items)" type_in: [ActivityType], "Filter by the type of activity (max 10,000 items)" type_not_in: [ActivityType], "Filter by the time the activity was created" createdAt_greater: Int, "Filter by the time the activity was created" createdAt_lesser: Int, "The order the results will be returned in" sort: [ActivitySort]): [ActivityUnion] + + activityReplies("Filter by the reply id" id: Int, "Filter by the parent id" activityId: Int): [ActivityReply] + + threads("Filter by the thread id" id: Int, "Filter by the user id of the thread's creator" userId: Int, "Filter by the user id of the last user to comment on the thread" replyUserId: Int, "Filter by if the currently authenticated user's subscribed threads" subscribed: Boolean, "Filter by thread category id" categoryId: Int, "Filter by thread media id category" mediaCategoryId: Int, "Filter by search query" search: String, "Filter by the thread id (max 10,000 items)" id_in: [Int], "The order the results will be returned in" sort: [ThreadSort]): [Thread] + + threadComments("Filter by the comment id" id: Int, "Filter by the thread id" threadId: Int, "Filter by the user id of the comment's creator" userId: Int, "The order the results will be returned in" sort: [ThreadCommentSort]): [ThreadComment] + + reviews("Filter by Review id" id: Int, "Filter by media id" mediaId: Int, "Filter by user id" userId: Int, "Filter by media type" mediaType: MediaType, "The order the results will be returned in" sort: [ReviewSort]): [Review] + + recommendations("Filter by recommendation id" id: Int, "Filter by media id" mediaId: Int, "Filter by media recommendation id" mediaRecommendationId: Int, "Filter by user who created the recommendation" userId: Int, "Filter by total rating of the recommendation" rating: Int, "Filter by the media on the authenticated user's lists" onList: Boolean, "Filter by total rating of the recommendation" rating_greater: Int, "Filter by total rating of the recommendation" rating_lesser: Int, "The order the results will be returned in" sort: [RecommendationSort]): [Recommendation] + + likes("The id of the likeable type" likeableId: Int, "The type of model the id applies to" type: LikeableType): [User] +} + +type PageInfo { + """ + The total number of items. Note: This value is not guaranteed to be accurate, do not rely on this for logic + """ + total: Int + + """ + The count on a page + """ + perPage: Int + + """ + The current page + """ + currentPage: Int + + """ + The last page + """ + lastPage: Int + + """ + If there is another page + """ + hasNextPage: Boolean +} + +""" +The `Boolean` scalar type represents `true` or `false`. +""" +scalar Boolean + +""" +The `String` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar String + +""" +User sort enums +""" +enum UserSort { + ID + + ID_DESC + + USERNAME + + USERNAME_DESC + + WATCHED_TIME + + WATCHED_TIME_DESC + + CHAPTERS_READ + + CHAPTERS_READ_DESC + + SEARCH_MATCH +} + +""" +A user +""" +type User { + """ + The id of the user + """ + id: Int! + + """ + The name of the user + """ + name: String! + + """ + The bio written by user (Markdown) + """ + about("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The user's avatar images + """ + avatar: UserAvatar + + """ + The user's banner images + """ + bannerImage: String + + """ + If the authenticated user if following this user + """ + isFollowing: Boolean + + """ + If this user if following the authenticated user + """ + isFollower: Boolean + + """ + If the user is blocked by the authenticated user + """ + isBlocked: Boolean + + """ + List of active bans. Mod-only + """ + bans: Json + + """ + The user's general options + """ + options: UserOptions + + """ + The user's media list options + """ + mediaListOptions: MediaListOptions + + """ + The users favourites + """ + favourites("Deprecated. Use page arguments on each favourite field instead." page: Int): Favourites + + """ + The users anime & manga list statistics + """ + statistics: UserStatisticTypes + + """ + The number of unread notifications the user has + """ + unreadNotificationCount: Int + + """ + The url for the user page on the AniList website + """ + siteUrl: String + + """ + The donation tier of the user + """ + donatorTier: Int + + """ + Custom donation badge text + """ + donatorBadge: String + + """ + The user's moderator roles if they are a site moderator + """ + moderatorRoles: [ModRole] + + """ + When the user's account was created. (Does not exist for accounts created before 2020) + """ + createdAt: Int + + """ + When the user's data was last updated + """ + updatedAt: Int + + """ + The user's statistics + """ + stats: UserStats @deprecated(reason: "Deprecated. Replaced with statistics field.") + + """ + If the user is a moderator or data moderator + """ + moderatorStatus: String @deprecated(reason: "Deprecated. Replaced with moderatorRoles field.") + + """ + The user's previously used names. + """ + previousNames: [UserPreviousName] +} + +""" +A user's avatars +""" +type UserAvatar { + """ + The avatar of user at its largest size + """ + large: String + + """ + The avatar of user at medium size + """ + medium: String +} + +"""""" +scalar Json + +""" +A user's general options +""" +type UserOptions { + """ + The language the user wants to see media titles in + """ + titleLanguage: UserTitleLanguage + + """ + Whether the user has enabled viewing of 18+ content + """ + displayAdultContent: Boolean + + """ + Whether the user receives notifications when a show they are watching aires + """ + airingNotifications: Boolean + + """ + Profile highlight color (blue, purple, pink, orange, red, green, gray) + """ + profileColor: String + + """ + Notification options + """ + notificationOptions: [NotificationOption] + + """ + The user's timezone offset (Auth user only) + """ + timezone: String + + """ + Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. + """ + activityMergeTime: Int + + """ + The language the user wants to see staff and character names in + """ + staffNameLanguage: UserStaffNameLanguage + + """ + Whether the user only allow messages from users they follow + """ + restrictMessagesToFollowing: Boolean + + """ + The list activity types the user has disabled from being created from list updates + """ + disabledListActivity: [ListActivityOption] +} + +""" +The language the user wants to see media titles in +""" +enum UserTitleLanguage { + """ + The romanization of the native language title + """ + ROMAJI + + """ + The official english title + """ + ENGLISH + + """ + Official title in it's native language + """ + NATIVE + + """ + The romanization of the native language title, stylised by media creator + """ + ROMAJI_STYLISED + + """ + The official english title, stylised by media creator + """ + ENGLISH_STYLISED + + """ + Official title in it's native language, stylised by media creator + """ + NATIVE_STYLISED +} + +""" +Notification option +""" +type NotificationOption { + """ + The type of notification + """ + type: NotificationType + + """ + Whether this type of notification is enabled + """ + enabled: Boolean +} + +""" +Notification type enum +""" +enum NotificationType { + """ + A user has sent you message + """ + ACTIVITY_MESSAGE + + """ + A user has replied to your activity + """ + ACTIVITY_REPLY + + """ + A user has followed you + """ + FOLLOWING + + """ + A user has mentioned you in their activity + """ + ACTIVITY_MENTION + + """ + A user has mentioned you in a forum comment + """ + THREAD_COMMENT_MENTION + + """ + A user has commented in one of your subscribed forum threads + """ + THREAD_SUBSCRIBED + + """ + A user has replied to your forum comment + """ + THREAD_COMMENT_REPLY + + """ + An anime you are currently watching has aired + """ + AIRING + + """ + A user has liked your activity + """ + ACTIVITY_LIKE + + """ + A user has liked your activity reply + """ + ACTIVITY_REPLY_LIKE + + """ + A user has liked your forum thread + """ + THREAD_LIKE + + """ + A user has liked your forum comment + """ + THREAD_COMMENT_LIKE + + """ + A user has replied to activity you have also replied to + """ + ACTIVITY_REPLY_SUBSCRIBED + + """ + A new anime or manga has been added to the site where its related media is on the user's list + """ + RELATED_MEDIA_ADDITION + + """ + An anime or manga has had a data change that affects how a user may track it in their lists + """ + MEDIA_DATA_CHANGE + + """ + Anime or manga entries on the user's list have been merged into a single entry + """ + MEDIA_MERGE + + """ + An anime or manga on the user's list has been deleted from the site + """ + MEDIA_DELETION + + """ + A user's submission has been accepted, partially accepted, or rejected + """ + MEDIA_SUBMISSION_UPDATE + + """ + A user's staff submission has been accepted, partially accepted, or rejected + """ + STAFF_SUBMISSION_UPDATE + + """ + A user's character submission has been accepted, partially accepted, or rejected + """ + CHARACTER_SUBMISSION_UPDATE +} + +""" +The language the user wants to see staff and character names in +""" +enum UserStaffNameLanguage { + """ + The romanization of the staff or character's native name, with western name ordering + """ + ROMAJI_WESTERN + + """ + The romanization of the staff or character's native name + """ + ROMAJI + + """ + The staff or character's name in their native language + """ + NATIVE +} + +type ListActivityOption { + disabled: Boolean + + type: MediaListStatus +} + +""" +Media list watching/reading status enum. +""" +enum MediaListStatus { + """ + Currently watching/reading + """ + CURRENT + + """ + Planning to watch/read + """ + PLANNING + + """ + Finished watching/reading + """ + COMPLETED + + """ + Stopped watching/reading before completing + """ + DROPPED + + """ + Paused watching/reading + """ + PAUSED + + """ + Re-watching/reading + """ + REPEATING +} + +""" +A user's list options +""" +type MediaListOptions { + """ + The score format the user is using for media lists + """ + scoreFormat: ScoreFormat + + """ + The default order list rows should be displayed in + """ + rowOrder: String + + useLegacyLists: Boolean @deprecated(reason: "No longer used") + + """ + The user's anime list options + """ + animeList: MediaListTypeOptions + + """ + The user's manga list options + """ + mangaList: MediaListTypeOptions + + """ + The list theme options for both lists + """ + sharedTheme: Json @deprecated(reason: "No longer used") + + """ + If the shared theme should be used instead of the individual list themes + """ + sharedThemeEnabled: Boolean @deprecated(reason: "No longer used") +} + +""" +Media list scoring type +""" +enum ScoreFormat { + """ + An integer from 0-100 + """ + POINT_100 + + """ + A float from 0-10 with 1 decimal place + """ + POINT_10_DECIMAL + + """ + An integer from 0-10 + """ + POINT_10 + + """ + An integer from 0-5. Should be represented in Stars + """ + POINT_5 + + """ + An integer from 0-3. Should be represented in Smileys. 0 => No Score, 1 => :(, 2 => :|, 3 => :) + """ + POINT_3 +} + +""" +A user's list options for anime or manga lists +""" +type MediaListTypeOptions { + """ + The order each list should be displayed in + """ + sectionOrder: [String] + + """ + If the completed sections of the list should be separated by format + """ + splitCompletedSectionByFormat: Boolean + + """ + The list theme options + """ + theme: Json @deprecated(reason: "This field has not yet been fully implemented and may change without warning") + + """ + The names of the user's custom lists + """ + customLists: [String] + + """ + The names of the user's advanced scoring sections + """ + advancedScoring: [String] + + """ + If advanced scoring is enabled + """ + advancedScoringEnabled: Boolean +} + +""" +User's favourite anime, manga, characters, staff & studios +""" +type Favourites { + """ + Favourite anime + """ + anime("The page number" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaConnection + + """ + Favourite manga + """ + manga("The page number" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaConnection + + """ + Favourite characters + """ + characters("The page number" page: Int, "The amount of entries per page, max 25" perPage: Int): CharacterConnection + + """ + Favourite staff + """ + staff("The page number" page: Int, "The amount of entries per page, max 25" perPage: Int): StaffConnection + + """ + Favourite studios + """ + studios("The page number" page: Int, "The amount of entries per page, max 25" perPage: Int): StudioConnection +} + +type MediaConnection { + edges: [MediaEdge] + + nodes: [Media] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Media connection edge +""" +type MediaEdge { + node: Media + + """ + The id of the connection + """ + id: Int + + """ + The type of relation to the parent model + """ + relationType("Provide 3 to use new version 3 of relation enum" version: Int): MediaRelation + + """ + If the studio is the main animation studio of the media (For Studio->MediaConnection field only) + """ + isMainStudio: Boolean! + + """ + The characters in the media voiced by the parent actor + """ + characters: [Character] + + """ + The characters role in the media + """ + characterRole: CharacterRole + + """ + Media specific character name + """ + characterName: String + + """ + Notes regarding the VA's role for the character + """ + roleNotes: String + + """ + Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant. + """ + dubGroup: String + + """ + The role of the staff member in the production of the media + """ + staffRole: String + + """ + The voice actors of the character + """ + voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff] + + """ + The voice actors of the character with role date + """ + voiceActorRoles(language: StaffLanguage, sort: [StaffSort]): [StaffRoleType] + + """ + The order the media should be displayed from the users favourites + """ + favouriteOrder: Int +} + +""" +Anime or Manga +""" +type Media { + """ + The id of the media + """ + id: Int! + + """ + The mal id of the media + """ + idMal: Int + + """ + The official titles of the media in various languages + """ + title: MediaTitle + + """ + The type of the media; anime or manga + """ + type: MediaType + + """ + The format the media was released in + """ + format: MediaFormat + + """ + The current releasing status of the media + """ + status("Provide 2 to use new version 2 of sources enum" version: Int): MediaStatus + + """ + Short description of the media's story and characters + """ + description("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The first official release date of the media + """ + startDate: FuzzyDate + + """ + The last official release date of the media + """ + endDate: FuzzyDate + + """ + The season the media was initially released in + """ + season: MediaSeason + + """ + The season year the media was initially released in + """ + seasonYear: Int + + """ + The year & season the media was initially released in + """ + seasonInt: Int @deprecated(reason: "") + + """ + The amount of episodes the anime has when complete + """ + episodes: Int + + """ + The general length of each anime episode in minutes + """ + duration: Int + + """ + The amount of chapters the manga has when complete + """ + chapters: Int + + """ + The amount of volumes the manga has when complete + """ + volumes: Int + + """ + Where the media was created. (ISO 3166-1 alpha-2) + """ + countryOfOrigin: CountryCode + + """ + If the media is officially licensed or a self-published doujin release + """ + isLicensed: Boolean + + """ + Source type the media was adapted from. + """ + source("Provide 2 or 3 to use new version 2 or 3 of sources enum" version: Int): MediaSource + + """ + Official Twitter hashtags for the media + """ + hashtag: String + + """ + Media trailer or advertisement + """ + trailer: MediaTrailer + + """ + When the media's data was last updated + """ + updatedAt: Int + + """ + The cover images of the media + """ + coverImage: MediaCoverImage + + """ + The banner image of the media + """ + bannerImage: String + + """ + The genres of the media + """ + genres: [String] + + """ + Alternative titles of the media + """ + synonyms: [String] + + """ + A weighted average score of all the user's scores of the media + """ + averageScore: Int + + """ + Mean score of all the user's scores of the media + """ + meanScore: Int + + """ + The number of users with the media on their list + """ + 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. + """ + isLocked: Boolean + + """ + The amount of related activity in the past hour + """ + trending: Int + + """ + The amount of user's who have favourited the media + """ + favourites: Int + + """ + List of tags that describes elements and themes of the media + """ + tags: [MediaTag] + + """ + Other media in the same or connecting franchise + """ + relations: MediaConnection + + """ + The characters in the media + """ + characters(sort: [CharacterSort], role: CharacterRole, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): CharacterConnection + + """ + The staff who produced the media + """ + staff(sort: [StaffSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): StaffConnection + + """ + The companies who produced the media + """ + studios(sort: [StudioSort], isMain: Boolean): StudioConnection + + """ + If the media is marked as favourite by the current authenticated user + """ + isFavourite: Boolean! + + """ + If the media is blocked from being added to favourites + """ + isFavouriteBlocked: Boolean! + + """ + If the media is intended only for 18+ adult audiences + """ + isAdult: Boolean + + """ + The media's next episode airing schedule + """ + nextAiringEpisode: AiringSchedule + + """ + The media's entire airing schedule + """ + airingSchedule("Filter to episodes that have not yet aired" notYetAired: Boolean, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): AiringScheduleConnection + + """ + The media's daily trend stats + """ + trends(sort: [MediaTrendSort], "Filter to stats recorded while the media was releasing" releasing: Boolean, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaTrendConnection + + """ + External links to another site related to the media + """ + externalLinks: [MediaExternalLink] + + """ + Data and links to legal streaming episodes on external sites + """ + streamingEpisodes: [MediaStreamingEpisode] + + """ + The ranking of the media in a particular time span and format compared to other media + """ + rankings: [MediaRank] + + """ + The authenticated user's media list entry for the media + """ + mediaListEntry: MediaList + + """ + User reviews of the media + """ + reviews(limit: Int, sort: [ReviewSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): ReviewConnection + + """ + User recommendations for similar media + """ + recommendations(sort: [RecommendationSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): RecommendationConnection + + stats: MediaStats + + """ + The url for the media page on the AniList website + """ + siteUrl: String + + """ + If the media should have forum thread automatically created for it on airing episode release + """ + autoCreateForumThread: Boolean + + """ + If the media is blocked from being recommended to/from + """ + isRecommendationBlocked: Boolean + + """ + If the media is blocked from being reviewed + """ + isReviewBlocked: Boolean + + """ + Notes for site moderators + """ + modNotes: String +} + +""" +The official titles of the media in various languages +""" +type MediaTitle { + """ + The romanization of the native language title + """ + romaji(stylised: Boolean): String + + """ + The official english title + """ + english(stylised: Boolean): String + + """ + Official title in it's native language + """ + native(stylised: Boolean): String + + """ + The currently authenticated users preferred title language. Default romaji for non-authenticated + """ + userPreferred: String +} + +""" +Media type enum, anime or manga. +""" +enum MediaType { + """ + Japanese Anime + """ + ANIME + + """ + Asian comic + """ + MANGA +} + +""" +The format the media was released in +""" +enum MediaFormat { + """ + Anime broadcast on television + """ + TV + + """ + Anime which are under 15 minutes in length and broadcast on television + """ + TV_SHORT + + """ + Anime movies with a theatrical release + """ + MOVIE + + """ + Special episodes that have been included in DVD/Blu-ray releases, picture dramas, pilots, etc + """ + SPECIAL + + """ + (Original Video Animation) Anime that have been released directly on DVD/Blu-ray without originally going through a theatrical release or television broadcast + """ + OVA + + """ + (Original Net Animation) Anime that have been originally released online or are only available through streaming services. + """ + ONA + + """ + Short anime released as a music video + """ + MUSIC + + """ + Professionally published manga with more than one chapter + """ + MANGA + + """ + Written books released as a series of light novels + """ + NOVEL + + """ + Manga with just one chapter + """ + ONE_SHOT +} + +""" +The current releasing status of the media +""" +enum MediaStatus { + """ + Has completed and is no longer being released + """ + FINISHED + + """ + Currently releasing + """ + RELEASING + + """ + To be released at a later date + """ + NOT_YET_RELEASED + + """ + Ended before the work could be finished + """ + CANCELLED + + """ + Version 2 only. Is currently paused from releasing and will resume at a later date + """ + HIATUS +} + +""" +Date object that allows for incomplete date values (fuzzy) +""" +type FuzzyDate { + """ + Numeric Year (2017) + """ + year: Int + + """ + Numeric Month (3) + """ + month: Int + + """ + Numeric Day (24) + """ + day: Int +} + +enum MediaSeason { + """ + Predominantly started airing between January and March + """ + WINTER + + """ + Predominantly started airing between April and June + """ + SPRING + + """ + Predominantly started airing between July and September + """ + SUMMER + + """ + Predominantly started airing between October and November + """ + FALL +} + +""" +ISO 3166-1 alpha-2 country code +""" +scalar CountryCode + +""" +Source type the media was adapted from +""" +enum MediaSource { + """ + An original production not based of another work + """ + ORIGINAL + + """ + Asian comic book + """ + MANGA + + """ + Written work published in volumes + """ + LIGHT_NOVEL + + """ + Video game driven primary by text and narrative + """ + VISUAL_NOVEL + + """ + Video game + """ + VIDEO_GAME + + """ + Other + """ + OTHER + + """ + Version 2+ only. Written works not published in volumes + """ + NOVEL + + """ + Version 2+ only. Self-published works + """ + DOUJINSHI + + """ + Version 2+ only. Japanese Anime + """ + ANIME + + """ + Version 3 only. Written works published online + """ + WEB_NOVEL + + """ + Version 3 only. Live action media such as movies or TV show + """ + LIVE_ACTION + + """ + Version 3 only. Games excluding video games + """ + GAME + + """ + Version 3 only. Comics excluding manga + """ + COMIC + + """ + Version 3 only. Multimedia project + """ + MULTIMEDIA_PROJECT + + """ + Version 3 only. Picture book + """ + PICTURE_BOOK +} + +""" +Media trailer or advertisement +""" +type MediaTrailer { + """ + The trailer video id + """ + id: String + + """ + The site the video is hosted by (Currently either youtube or dailymotion) + """ + site: String + + """ + The url for the thumbnail image of the video + """ + thumbnail: String +} + +type MediaCoverImage { + """ + The cover image url of the media at its largest size. If this size isn't available, large will be provided instead. + """ + extraLarge: String + + """ + The cover image url of the media at a large size + """ + large: String + + """ + The cover image url of the media at medium size + """ + medium: String + + """ + Average #hex color of cover image + """ + color: String +} + +""" +A tag that describes a theme or element of the media +""" +type MediaTag { + """ + The id of the tag + """ + id: Int! + + """ + The name of the tag + """ + name: String! + + """ + A general description of the tag + """ + description: String + + """ + The categories of tags this tag belongs to + """ + category: String + + """ + The relevance ranking of the tag out of the 100 for this media + """ + rank: Int + + """ + If the tag could be a spoiler for any media + """ + isGeneralSpoiler: Boolean + + """ + If the tag is a spoiler for this media + """ + isMediaSpoiler: Boolean + + """ + If the tag is only for adult 18+ media + """ + isAdult: Boolean + + """ + The user who submitted the tag + """ + userId: Int +} + +""" +Character sort enums +""" +enum CharacterSort { + ID + + ID_DESC + + ROLE + + ROLE_DESC + + SEARCH_MATCH + + FAVOURITES + + FAVOURITES_DESC + + """ + Order manually decided by moderators + """ + RELEVANCE +} + +""" +The role the character plays in the media +""" +enum CharacterRole { + """ + A primary character role in the media + """ + MAIN + + """ + A supporting character role in the media + """ + SUPPORTING + + """ + A background character in the media + """ + BACKGROUND +} + +type CharacterConnection { + edges: [CharacterEdge] + + nodes: [Character] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Character connection edge +""" +type CharacterEdge { + node: Character + + """ + The id of the connection + """ + id: Int + + """ + The characters role in the media + """ + role: CharacterRole + + """ + Media specific character name + """ + name: String + + """ + The voice actors of the character + """ + voiceActors(language: StaffLanguage, sort: [StaffSort]): [Staff] + + """ + The voice actors of the character with role date + """ + voiceActorRoles(language: StaffLanguage, sort: [StaffSort]): [StaffRoleType] + + """ + The media the character is in + """ + media: [Media] + + """ + The order the character should be displayed from the users favourites + """ + favouriteOrder: Int +} + +""" +A character that features in an anime or manga +""" +type Character { + """ + The id of the character + """ + id: Int! + + """ + The names of the character + """ + name: CharacterName + + """ + Character images + """ + image: CharacterImage + + """ + A general description of the character + """ + description("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The character's gender. Usually Male, Female, or Non-binary but can be any string. + """ + gender: String + + """ + The character's birth date + """ + dateOfBirth: FuzzyDate + + """ + The character's age. Note this is a string, not an int, it may contain further text and additional ages. + """ + age: String + + """ + The characters blood type + """ + bloodType: String + + """ + If the character is marked as favourite by the currently authenticated user + """ + isFavourite: Boolean! + + """ + If the character is blocked from being added to favourites + """ + isFavouriteBlocked: Boolean! + + """ + The url for the character page on the AniList website + """ + siteUrl: String + + """ + Media that includes the character + """ + media(sort: [MediaSort], type: MediaType, onList: Boolean, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaConnection + + updatedAt: Int @deprecated(reason: "No data available") + + """ + The amount of user's who have favourited the character + """ + favourites: Int + + """ + Notes for site moderators + """ + modNotes: String +} + +""" +The names of the character +""" +type CharacterName { + """ + The character's given name + """ + first: String + + """ + The character's middle name + """ + middle: String + + """ + The character's surname + """ + last: String + + """ + The character's first and last name + """ + full: String + + """ + The character's full name in their native language + """ + native: String + + """ + Other names the character might be referred to as + """ + alternative: [String] + + """ + Other names the character might be referred to as but are spoilers + """ + alternativeSpoiler: [String] + + """ + The currently authenticated users preferred name language. Default romaji for non-authenticated + """ + userPreferred: String +} + +type CharacterImage { + """ + The character's image of media at its largest size + """ + large: String + + """ + The character's image of media at medium size + """ + medium: String +} + +""" +Media sort enums +""" +enum MediaSort { + ID + + ID_DESC + + TITLE_ROMAJI + + TITLE_ROMAJI_DESC + + TITLE_ENGLISH + + TITLE_ENGLISH_DESC + + TITLE_NATIVE + + TITLE_NATIVE_DESC + + TYPE + + TYPE_DESC + + FORMAT + + FORMAT_DESC + + START_DATE + + START_DATE_DESC + + END_DATE + + END_DATE_DESC + + SCORE + + SCORE_DESC + + POPULARITY + + POPULARITY_DESC + + TRENDING + + TRENDING_DESC + + EPISODES + + EPISODES_DESC + + DURATION + + DURATION_DESC + + STATUS + + STATUS_DESC + + CHAPTERS + + CHAPTERS_DESC + + VOLUMES + + VOLUMES_DESC + + UPDATED_AT + + UPDATED_AT_DESC + + SEARCH_MATCH + + FAVOURITES + + FAVOURITES_DESC +} + +""" +The primary language of the voice actor +""" +enum StaffLanguage { + """ + Japanese + """ + JAPANESE + + """ + English + """ + ENGLISH + + """ + Korean + """ + KOREAN + + """ + Italian + """ + ITALIAN + + """ + Spanish + """ + SPANISH + + """ + Portuguese + """ + PORTUGUESE + + """ + French + """ + FRENCH + + """ + German + """ + GERMAN + + """ + Hebrew + """ + HEBREW + + """ + Hungarian + """ + HUNGARIAN +} + +""" +Staff sort enums +""" +enum StaffSort { + ID + + ID_DESC + + ROLE + + ROLE_DESC + + LANGUAGE + + LANGUAGE_DESC + + SEARCH_MATCH + + FAVOURITES + + FAVOURITES_DESC + + """ + Order manually decided by moderators + """ + RELEVANCE +} + +""" +Voice actors or production staff +""" +type Staff { + """ + The id of the staff member + """ + id: Int! + + """ + The names of the staff member + """ + name: StaffName + + """ + The primary language the staff member dub's in + """ + language: StaffLanguage @deprecated(reason: "Replaced with languageV2") + + """ + 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 + """ + languageV2: String + + """ + The staff images + """ + image: StaffImage + + """ + A general description of the staff member + """ + description("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The person's primary occupations + """ + primaryOccupations: [String] + + """ + The staff's gender. Usually Male, Female, or Non-binary but can be any string. + """ + gender: String + + dateOfBirth: FuzzyDate + + dateOfDeath: FuzzyDate + + """ + The person's age in years + """ + age: Int + + """ + [startYear, endYear] (If the 2nd value is not present staff is still active) + """ + yearsActive: [Int] + + """ + The persons birthplace or hometown + """ + homeTown: String + + """ + The persons blood type + """ + bloodType: String + + """ + If the staff member is marked as favourite by the currently authenticated user + """ + isFavourite: Boolean! + + """ + If the staff member is blocked from being added to favourites + """ + isFavouriteBlocked: Boolean! + + """ + The url for the staff page on the AniList website + """ + siteUrl: String + + """ + Media where the staff member has a production role + """ + staffMedia(sort: [MediaSort], type: MediaType, onList: Boolean, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaConnection + + """ + Characters voiced by the actor + """ + characters(sort: [CharacterSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): CharacterConnection + + """ + Media the actor voiced characters in. (Same data as characters with media as node instead of characters) + """ + characterMedia(sort: [MediaSort], onList: Boolean, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaConnection + + updatedAt: Int @deprecated(reason: "No data available") + + """ + Staff member that the submission is referencing + """ + staff: Staff + + """ + Submitter for the submission + """ + submitter: User + + """ + Status of the submission + """ + submissionStatus: Int + + """ + Inner details of submission status + """ + submissionNotes: String + + """ + The amount of user's who have favourited the staff member + """ + favourites: Int + + """ + Notes for site moderators + """ + modNotes: String +} + +""" +The names of the staff member +""" +type StaffName { + """ + The person's given name + """ + first: String + + """ + The person's middle name + """ + middle: String + + """ + The person's surname + """ + last: String + + """ + The person's first and last name + """ + full: String + + """ + The person's full name in their native language + """ + native: String + + """ + Other names the staff member might be referred to as (pen names) + """ + alternative: [String] + + """ + The currently authenticated users preferred name language. Default romaji for non-authenticated + """ + userPreferred: String +} + +type StaffImage { + """ + The person's image of media at its largest size + """ + large: String + + """ + The person's image of media at medium size + """ + medium: String +} + +""" +Voice actor role for a character +""" +type StaffRoleType { + """ + The voice actors of the character + """ + voiceActor: Staff + + """ + Notes regarding the VA's role for the character + """ + roleNotes: String + + """ + Used for grouping roles where multiple dubs exist for the same language. Either dubbing company name or language variant. + """ + dubGroup: String +} + +type StaffConnection { + edges: [StaffEdge] + + nodes: [Staff] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Staff connection edge +""" +type StaffEdge { + node: Staff + + """ + The id of the connection + """ + id: Int + + """ + The role of the staff member in the production of the media + """ + role: String + + """ + The order the staff should be displayed from the users favourites + """ + favouriteOrder: Int +} + +""" +Studio sort enums +""" +enum StudioSort { + ID + + ID_DESC + + NAME + + NAME_DESC + + SEARCH_MATCH + + FAVOURITES + + FAVOURITES_DESC +} + +type StudioConnection { + edges: [StudioEdge] + + nodes: [Studio] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Studio connection edge +""" +type StudioEdge { + node: Studio + + """ + The id of the connection + """ + id: Int + + """ + If the studio is the main animation studio of the anime + """ + isMain: Boolean! + + """ + The order the character should be displayed from the users favourites + """ + favouriteOrder: Int +} + +""" +Animation or production company +""" +type Studio { + """ + The id of the studio + """ + id: Int! + + """ + The name of the studio + """ + name: String! + + """ + If the studio is an animation studio or a different kind of company + """ + isAnimationStudio: Boolean! + + """ + The media the studio has worked on + """ + media("The order the results will be returned in" sort: [MediaSort], "If the studio was the primary animation studio of the media" isMain: Boolean, onList: Boolean, "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): MediaConnection + + """ + The url for the studio page on the AniList website + """ + siteUrl: String + + """ + If the studio is marked as favourite by the currently authenticated user + """ + isFavourite: Boolean! + + """ + The amount of user's who have favourited the studio + """ + favourites: Int +} + +""" +Media Airing Schedule. NOTE: We only aim to guarantee that FUTURE airing data is present and accurate. +""" +type AiringSchedule { + """ + The id of the airing schedule item + """ + id: Int! + + """ + The time the episode airs at + """ + airingAt: Int! + + """ + Seconds until episode starts airing + """ + timeUntilAiring: Int! + + """ + The airing episode number + """ + episode: Int! + + """ + The associate media id of the airing episode + """ + mediaId: Int! + + """ + The associate media of the airing episode + """ + media: Media +} + +type AiringScheduleConnection { + edges: [AiringScheduleEdge] + + nodes: [AiringSchedule] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +AiringSchedule connection edge +""" +type AiringScheduleEdge { + node: AiringSchedule + + """ + The id of the connection + """ + id: Int +} + +""" +Media trend sort enums +""" +enum MediaTrendSort { + ID + + ID_DESC + + MEDIA_ID + + MEDIA_ID_DESC + + DATE + + DATE_DESC + + SCORE + + SCORE_DESC + + POPULARITY + + POPULARITY_DESC + + TRENDING + + TRENDING_DESC + + EPISODE + + EPISODE_DESC +} + +type MediaTrendConnection { + edges: [MediaTrendEdge] + + nodes: [MediaTrend] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Media trend connection edge +""" +type MediaTrendEdge { + node: MediaTrend +} + +""" +Daily media statistics +""" +type MediaTrend { + """ + The id of the tag + """ + mediaId: Int! + + """ + The day the data was recorded (timestamp) + """ + date: Int! + + """ + The amount of media activity on the day + """ + trending: Int! + + """ + A weighted average score of all the user's scores of the media + """ + averageScore: Int + + """ + The number of users with the media on their list + """ + popularity: Int + + """ + The number of users with watching/reading the media + """ + inProgress: Int + + """ + If the media was being released at this time + """ + releasing: Boolean! + + """ + The episode number of the anime released on this day + """ + episode: Int + + """ + The related media + """ + media: Media +} + +""" +An external link to another site related to the media or staff member +""" +type MediaExternalLink { + """ + The id of the external link + """ + id: Int! + + """ + The url of the external link or base url of link source + """ + url: String + + """ + The links website site name + """ + site: String! + + """ + The links website site id + """ + siteId: Int + + type: ExternalLinkType + + """ + Language the site content is in. See Staff language field for values. + """ + language: String + + color: String + + """ + The icon image url of the site. Not available for all links. Transparent PNG 64x64 + """ + icon: String + + notes: String + + isDisabled: Boolean +} + +enum ExternalLinkType { + INFO + + STREAMING + + SOCIAL +} + +""" +Data and links to legal streaming episodes on external sites +""" +type MediaStreamingEpisode { + """ + Title of the episode + """ + title: String + + """ + Url of episode image thumbnail + """ + thumbnail: String + + """ + The url of the episode + """ + url: String + + """ + The site location of the streaming episodes + """ + site: String +} + +""" +The ranking of a media in a particular time span and format compared to other media +""" +type MediaRank { + """ + The id of the rank + """ + id: Int! + + """ + The numerical rank of the media + """ + rank: Int! + + """ + The type of ranking + """ + type: MediaRankType! + + """ + The format the media is ranked within + """ + format: MediaFormat! + + """ + The year the media is ranked within + """ + year: Int + + """ + The season the media is ranked within + """ + season: MediaSeason + + """ + If the ranking is based on all time instead of a season/year + """ + allTime: Boolean + + """ + String that gives context to the ranking type and time span + """ + context: String! +} + +""" +The type of ranking +""" +enum MediaRankType { + """ + Ranking is based on the media's ratings/score + """ + RATED + + """ + Ranking is based on the media's popularity + """ + POPULAR +} + +""" +List of anime or manga +""" +type MediaList { + """ + The id of the list entry + """ + id: Int! + + """ + The id of the user owner of the list entry + """ + userId: Int! + + """ + The id of the media + """ + mediaId: Int! + + """ + The watching/reading status + """ + status: MediaListStatus + + """ + The score of the entry + """ + score("Force the score to be returned in the provided format type." format: ScoreFormat): Float + + """ + The amount of episodes/chapters consumed by the user + """ + progress: Int + + """ + The amount of volumes read by the user + """ + progressVolumes: Int + + """ + The amount of times the user has rewatched/read the media + """ + repeat: Int + + """ + Priority of planning + """ + priority: Int + + """ + If the entry should only be visible to authenticated user + """ + private: Boolean + + """ + Text notes + """ + notes: String + + """ + If the entry shown be hidden from non-custom lists + """ + hiddenFromStatusLists: Boolean + + """ + Map of booleans for which custom lists the entry are in + """ + customLists("Change return structure to an array of objects" asArray: Boolean): Json + + """ + Map of advanced scores with name keys + """ + advancedScores: Json + + """ + When the entry was started by the user + """ + startedAt: FuzzyDate + + """ + When the entry was completed by the user + """ + completedAt: FuzzyDate + + """ + When the entry data was last updated + """ + updatedAt: Int + + """ + When the entry data was created + """ + createdAt: Int + + media: Media + + user: User +} + +""" +The `Float` scalar type represents signed double-precision fractional +values as specified by +[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). +""" +scalar Float + +""" +Review sort enums +""" +enum ReviewSort { + ID + + ID_DESC + + SCORE + + SCORE_DESC + + RATING + + RATING_DESC + + CREATED_AT @deprecated(reason: "Use ID instead") + + CREATED_AT_DESC @deprecated(reason: "Use ID_DESC instead") + + UPDATED_AT + + UPDATED_AT_DESC +} + +type ReviewConnection { + edges: [ReviewEdge] + + nodes: [Review] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Review connection edge +""" +type ReviewEdge { + node: Review +} + +""" +A Review that features in an anime or manga +""" +type Review { + """ + The id of the review + """ + id: Int! + + """ + The id of the review's creator + """ + userId: Int! + + """ + The id of the review's media + """ + mediaId: Int! + + """ + For which type of media the review is for + """ + mediaType: MediaType + + """ + A short summary of the review + """ + summary: String + + """ + The main review body text + """ + body("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The total user rating of the review + """ + rating: Int + + """ + The amount of user ratings of the review + """ + ratingAmount: Int + + """ + The rating of the review by currently authenticated user + """ + userRating: ReviewRating + + """ + The review score of the media + """ + score: Int + + """ + If the review is not yet publicly published and is only viewable by creator + """ + private: Boolean + + """ + The url for the review page on the AniList website + """ + siteUrl: String + + """ + The time of the thread creation + """ + createdAt: Int! + + """ + The time of the thread last update + """ + updatedAt: Int! + + """ + The creator of the review + """ + user: User + + """ + The media the review is of + """ + media: Media +} + +""" +Review rating enums +""" +enum ReviewRating { + NO_VOTE + + UP_VOTE + + DOWN_VOTE +} + +""" +Recommendation sort enums +""" +enum RecommendationSort { + ID + + ID_DESC + + RATING + + RATING_DESC +} + +type RecommendationConnection { + edges: [RecommendationEdge] + + nodes: [Recommendation] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Recommendation connection edge +""" +type RecommendationEdge { + node: Recommendation +} + +""" +Media recommendation +""" +type Recommendation { + """ + The id of the recommendation + """ + id: Int! + + """ + Users rating of the recommendation + """ + rating: Int + + """ + The rating of the recommendation by currently authenticated user + """ + userRating: RecommendationRating + + """ + The media the recommendation is from + """ + media: Media + + """ + The recommended media + """ + mediaRecommendation: Media + + """ + The user that first created the recommendation + """ + user: User +} + +""" +Recommendation rating enums +""" +enum RecommendationRating { + NO_RATING + + RATE_UP + + RATE_DOWN +} + +""" +A media's statistics +""" +type MediaStats { + scoreDistribution: [ScoreDistribution] + + statusDistribution: [StatusDistribution] + + airingProgression: [AiringProgression] @deprecated(reason: "Replaced by MediaTrends") +} + +""" +A user's list score distribution. +""" +type ScoreDistribution { + score: Int + + """ + The amount of list entries with this score + """ + amount: Int +} + +""" +The distribution of the watching/reading status of media or a user's list +""" +type StatusDistribution { + """ + The day the activity took place (Unix timestamp) + """ + status: MediaListStatus + + """ + The amount of entries with this status + """ + amount: Int +} + +""" +Score & Watcher stats for airing anime by episode and mid-week +""" +type AiringProgression { + """ + The episode the stats were recorded at. .5 is the mid point between 2 episodes airing dates. + """ + episode: Float + + """ + The average score for the media + """ + score: Float + + """ + The amount of users watching the anime + """ + watching: Int +} + +""" +Type of relation media has to its parent. +""" +enum MediaRelation { + """ + An adaption of this media into a different format + """ + ADAPTATION + + """ + Released before the relation + """ + PREQUEL + + """ + Released after the relation + """ + SEQUEL + + """ + The media a side story is from + """ + PARENT + + """ + A side story of the parent media + """ + SIDE_STORY + + """ + Shares at least 1 character + """ + CHARACTER + + """ + A shortened and summarized version + """ + SUMMARY + + """ + An alternative version of the same media + """ + ALTERNATIVE + + """ + An alternative version of the media with a different primary focus + """ + SPIN_OFF + + """ + Other + """ + OTHER + + """ + Version 2 only. The source material the media was adapted from + """ + SOURCE + + """ + Version 2 only. + """ + COMPILATION + + """ + Version 2 only. + """ + CONTAINS + + """ + Version 3 only. The media is set in the same universe as another media + """ + SAME_UNIVERSE +} + +type UserStatisticTypes { + anime: UserStatistics + + manga: UserStatistics +} + +type UserStatistics { + count: Int! + + meanScore: Float! + + standardDeviation: Float! + + minutesWatched: Int! + + episodesWatched: Int! + + chaptersRead: Int! + + volumesRead: Int! + + formats(limit: Int, sort: [UserStatisticsSort]): [UserFormatStatistic] + + statuses(limit: Int, sort: [UserStatisticsSort]): [UserStatusStatistic] + + scores(limit: Int, sort: [UserStatisticsSort]): [UserScoreStatistic] + + lengths(limit: Int, sort: [UserStatisticsSort]): [UserLengthStatistic] + + releaseYears(limit: Int, sort: [UserStatisticsSort]): [UserReleaseYearStatistic] + + startYears(limit: Int, sort: [UserStatisticsSort]): [UserStartYearStatistic] + + genres(limit: Int, sort: [UserStatisticsSort]): [UserGenreStatistic] + + tags(limit: Int, sort: [UserStatisticsSort]): [UserTagStatistic] + + countries(limit: Int, sort: [UserStatisticsSort]): [UserCountryStatistic] + + voiceActors(limit: Int, sort: [UserStatisticsSort]): [UserVoiceActorStatistic] + + staff(limit: Int, sort: [UserStatisticsSort]): [UserStaffStatistic] + + studios(limit: Int, sort: [UserStatisticsSort]): [UserStudioStatistic] +} + +""" +User statistics sort enum +""" +enum UserStatisticsSort { + ID + + ID_DESC + + COUNT + + COUNT_DESC + + PROGRESS + + PROGRESS_DESC + + MEAN_SCORE + + MEAN_SCORE_DESC +} + +type UserFormatStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + format: MediaFormat +} + +type UserStatusStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + status: MediaListStatus +} + +type UserScoreStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + score: Int +} + +type UserLengthStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + length: String +} + +type UserReleaseYearStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + releaseYear: Int +} + +type UserStartYearStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + startYear: Int +} + +type UserGenreStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + genre: String +} + +type UserTagStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + tag: MediaTag +} + +type UserCountryStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + country: CountryCode +} + +type UserVoiceActorStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + voiceActor: Staff + + characterIds: [Int]! +} + +type UserStaffStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + staff: Staff +} + +type UserStudioStatistic { + count: Int! + + meanScore: Float! + + minutesWatched: Int! + + chaptersRead: Int! + + mediaIds: [Int]! + + studio: Studio +} + +""" +Mod role enums +""" +enum ModRole { + """ + An AniList administrator + """ + ADMIN + + """ + A head developer of AniList + """ + LEAD_DEVELOPER + + """ + An AniList developer + """ + DEVELOPER + + """ + A lead community moderator + """ + LEAD_COMMUNITY + + """ + A community moderator + """ + COMMUNITY + + """ + A discord community moderator + """ + DISCORD_COMMUNITY + + """ + A lead anime data moderator + """ + LEAD_ANIME_DATA + + """ + An anime data moderator + """ + ANIME_DATA + + """ + A lead manga data moderator + """ + LEAD_MANGA_DATA + + """ + A manga data moderator + """ + MANGA_DATA + + """ + A lead social media moderator + """ + LEAD_SOCIAL_MEDIA + + """ + A social media moderator + """ + SOCIAL_MEDIA + + """ + A retired moderator + """ + RETIRED + + """ + A character data moderator + """ + CHARACTER_DATA + + """ + A staff data moderator + """ + STAFF_DATA +} + +""" +A user's statistics +""" +type UserStats { + """ + The amount of anime the user has watched in minutes + """ + watchedTime: Int + + """ + The amount of manga chapters the user has read + """ + chaptersRead: Int + + activityHistory: [UserActivityHistory] + + animeStatusDistribution: [StatusDistribution] + + mangaStatusDistribution: [StatusDistribution] + + animeScoreDistribution: [ScoreDistribution] + + mangaScoreDistribution: [ScoreDistribution] + + animeListScores: ListScoreStats + + mangaListScores: ListScoreStats + + favouredGenresOverview: [GenreStats] + + favouredGenres: [GenreStats] + + favouredTags: [TagStats] + + favouredActors: [StaffStats] + + favouredStaff: [StaffStats] + + favouredStudios: [StudioStats] + + favouredYears: [YearStats] + + favouredFormats: [FormatStats] +} + +""" +A user's activity history stats for the previous 6 months. Refreshes only periodically +""" +type UserActivityHistory { + """ + The day the activity took place (Unix timestamp) + """ + date: Int + + """ + The amount of activity on the day + """ + amount: Int + + """ + The level of activity represented on a 1-10 scale + """ + level: Int +} + +""" +User's list score statistics +""" +type ListScoreStats { + meanScore: Int + + standardDeviation: Int +} + +""" +User's genre statistics +""" +type GenreStats { + genre: String + + amount: Int + + meanScore: Int + + """ + The amount of time in minutes the genre has been watched by the user + """ + timeWatched: Int +} + +""" +User's tag statistics +""" +type TagStats { + tag: MediaTag + + amount: Int + + meanScore: Int + + """ + The amount of time in minutes the tag has been watched by the user + """ + timeWatched: Int +} + +""" +User's staff statistics +""" +type StaffStats { + staff: Staff + + amount: Int + + meanScore: Int + + """ + The amount of time in minutes the staff member has been watched by the user + """ + timeWatched: Int +} + +""" +User's studio statistics +""" +type StudioStats { + studio: Studio + + amount: Int + + meanScore: Int + + """ + The amount of time in minutes the studio's works have been watched by the user + """ + timeWatched: Int +} + +""" +User's year statistics +""" +type YearStats { + year: Int + + amount: Int + + meanScore: Int +} + +""" +User's format statistics +""" +type FormatStats { + format: MediaFormat + + amount: Int +} + +""" +A user's previous name +""" +type UserPreviousName { + """ + A previous name of the user. + """ + name: String + + """ + When the user first changed from this name. + """ + createdAt: Int + + """ + When the user most recently changed from this name. + """ + updatedAt: Int +} + +""" +8 digit long date integer (YYYYMMDD). Unknown dates represented by 0. E.g. 2016: 20160000, May 1976: 19760500 +""" +scalar FuzzyDateInt + +""" +Media list sort enums +""" +enum MediaListSort { + MEDIA_ID + + MEDIA_ID_DESC + + SCORE + + SCORE_DESC + + STATUS + + STATUS_DESC + + PROGRESS + + PROGRESS_DESC + + PROGRESS_VOLUMES + + PROGRESS_VOLUMES_DESC + + REPEAT + + REPEAT_DESC + + PRIORITY + + PRIORITY_DESC + + STARTED_ON + + STARTED_ON_DESC + + FINISHED_ON + + FINISHED_ON_DESC + + ADDED_TIME + + ADDED_TIME_DESC + + UPDATED_TIME + + UPDATED_TIME_DESC + + MEDIA_TITLE_ROMAJI + + MEDIA_TITLE_ROMAJI_DESC + + MEDIA_TITLE_ENGLISH + + MEDIA_TITLE_ENGLISH_DESC + + MEDIA_TITLE_NATIVE + + MEDIA_TITLE_NATIVE_DESC + + MEDIA_POPULARITY + + MEDIA_POPULARITY_DESC +} + +""" +Airing schedule sort enums +""" +enum AiringSort { + ID + + ID_DESC + + MEDIA_ID + + MEDIA_ID_DESC + + TIME + + TIME_DESC + + EPISODE + + EPISODE_DESC +} + +""" +Notification union type +""" +union NotificationUnion = AiringNotification|FollowingNotification|ActivityMessageNotification|ActivityMentionNotification|ActivityReplyNotification|ActivityReplySubscribedNotification|ActivityLikeNotification|ActivityReplyLikeNotification|ThreadCommentMentionNotification|ThreadCommentReplyNotification|ThreadCommentSubscribedNotification|ThreadCommentLikeNotification|ThreadLikeNotification|RelatedMediaAdditionNotification|MediaDataChangeNotification|MediaMergeNotification|MediaDeletionNotification|MediaSubmissionUpdateNotification|StaffSubmissionUpdateNotification|CharacterSubmissionUpdateNotification + +""" +Notification for when an episode of anime airs +""" +type AiringNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the aired anime + """ + animeId: Int! + + """ + The episode number that just aired + """ + episode: Int! + + """ + The notification context text + """ + contexts: [String] + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The associated media of the airing schedule + """ + media: Media +} + +""" +Notification for when the authenticated user is followed by another user +""" +type FollowingNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who followed the authenticated user + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The liked activity + """ + user: User +} + +""" +Notification for when a user is send an activity message +""" +type ActivityMessageNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The if of the user who send the message + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity message + """ + activityId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The message activity + """ + message: MessageActivity + + """ + The user who sent the message + """ + user: User +} + +""" +User message activity +""" +type MessageActivity { + """ + The id of the activity + """ + id: Int! + + """ + The user id of the activity's recipient + """ + recipientId: Int + + """ + The user id of the activity's sender + """ + messengerId: Int + + """ + The type of the activity + """ + type: ActivityType + + """ + The number of activity replies + """ + replyCount: Int! + + """ + The message text (Markdown) + """ + message("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + If the activity is locked and can receive replies + """ + isLocked: Boolean + + """ + If the currently authenticated user is subscribed to the activity + """ + isSubscribed: Boolean + + """ + The amount of likes the activity has + """ + likeCount: Int! + + """ + If the currently authenticated user liked the activity + """ + isLiked: Boolean + + """ + If the activity is pinned to the top of the users activity feed + """ + isPinned: Boolean + + """ + If the message is private and only viewable to the sender and recipients + """ + isPrivate: Boolean + + """ + The url for the activity page on the AniList website + """ + siteUrl: String + + """ + The time the activity was created at + """ + createdAt: Int! + + """ + The user who the activity message was sent to + """ + recipient: User + + """ + The user who sent the activity message + """ + messenger: User + + """ + The written replies to the activity + """ + replies: [ActivityReply] + + """ + The users who liked the activity + """ + likes: [User] +} + +""" +Activity type enum. +""" +enum ActivityType { + """ + A text activity + """ + TEXT + + """ + A anime list update activity + """ + ANIME_LIST + + """ + A manga list update activity + """ + MANGA_LIST + + """ + A text message activity sent to another user + """ + MESSAGE + + """ + Anime & Manga list update, only used in query arguments + """ + MEDIA_LIST +} + +""" +Replay to an activity item +""" +type ActivityReply { + """ + The id of the reply + """ + id: Int! + + """ + The id of the replies creator + """ + userId: Int + + """ + The id of the parent activity + """ + activityId: Int + + """ + The reply text + """ + text("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The amount of likes the reply has + """ + likeCount: Int! + + """ + If the currently authenticated user liked the reply + """ + isLiked: Boolean + + """ + The time the reply was created at + """ + createdAt: Int! + + """ + The user who created reply + """ + user: User + + """ + The users who liked the reply + """ + likes: [User] +} + +""" +Notification for when authenticated user is @ mentioned in activity or reply +""" +type ActivityMentionNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who mentioned the authenticated user + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity where mentioned + """ + activityId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The liked activity + """ + activity: ActivityUnion + + """ + The user who mentioned the authenticated user + """ + user: User +} + +""" +Activity union type +""" +union ActivityUnion = TextActivity|ListActivity|MessageActivity + +""" +User text activity +""" +type TextActivity { + """ + The id of the activity + """ + id: Int! + + """ + The user id of the activity's creator + """ + userId: Int + + """ + The type of activity + """ + type: ActivityType + + """ + The number of activity replies + """ + replyCount: Int! + + """ + The status text (Markdown) + """ + text("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The url for the activity page on the AniList website + """ + siteUrl: String + + """ + If the activity is locked and can receive replies + """ + isLocked: Boolean + + """ + If the currently authenticated user is subscribed to the activity + """ + isSubscribed: Boolean + + """ + The amount of likes the activity has + """ + likeCount: Int! + + """ + If the currently authenticated user liked the activity + """ + isLiked: Boolean + + """ + If the activity is pinned to the top of the users activity feed + """ + isPinned: Boolean + + """ + The time the activity was created at + """ + createdAt: Int! + + """ + The user who created the activity + """ + user: User + + """ + The written replies to the activity + """ + replies: [ActivityReply] + + """ + The users who liked the activity + """ + likes: [User] +} + +""" +User list activity (anime & manga updates) +""" +type ListActivity { + """ + The id of the activity + """ + id: Int! + + """ + The user id of the activity's creator + """ + userId: Int + + """ + The type of activity + """ + type: ActivityType + + """ + The number of activity replies + """ + replyCount: Int! + + """ + The list item's textual status + """ + status: String + + """ + The list progress made + """ + progress: String + + """ + If the activity is locked and can receive replies + """ + isLocked: Boolean + + """ + If the currently authenticated user is subscribed to the activity + """ + isSubscribed: Boolean + + """ + The amount of likes the activity has + """ + likeCount: Int! + + """ + If the currently authenticated user liked the activity + """ + isLiked: Boolean + + """ + If the activity is pinned to the top of the users activity feed + """ + isPinned: Boolean + + """ + The url for the activity page on the AniList website + """ + siteUrl: String + + """ + The time the activity was created at + """ + createdAt: Int! + + """ + The owner of the activity + """ + user: User + + """ + The associated media to the activity update + """ + media: Media + + """ + The written replies to the activity + """ + replies: [ActivityReply] + + """ + The users who liked the activity + """ + likes: [User] +} + +""" +Notification for when a user replies to the authenticated users activity +""" +type ActivityReplyNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who replied to the activity + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity which was replied too + """ + activityId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The liked activity + """ + activity: ActivityUnion + + """ + The user who replied to the activity + """ + user: User +} + +""" +Notification for when a user replies to activity the authenticated user has replied to +""" +type ActivityReplySubscribedNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who replied to the activity + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity which was replied too + """ + activityId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The liked activity + """ + activity: ActivityUnion + + """ + The user who replied to the activity + """ + user: User +} + +""" +Notification for when a activity is liked +""" +type ActivityLikeNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who liked to the activity + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity which was liked + """ + activityId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The liked activity + """ + activity: ActivityUnion + + """ + The user who liked the activity + """ + user: User +} + +""" +Notification for when a activity reply is liked +""" +type ActivityReplyLikeNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who liked to the activity reply + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity where the reply which was liked + """ + activityId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The liked activity + """ + activity: ActivityUnion + + """ + The user who liked the activity reply + """ + user: User +} + +""" +Notification for when authenticated user is @ mentioned in a forum thread comment +""" +type ThreadCommentMentionNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who mentioned the authenticated user + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the comment where mentioned + """ + commentId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The thread that the relevant comment belongs to + """ + thread: Thread + + """ + The thread comment that included the @ mention + """ + comment: ThreadComment + + """ + The user who mentioned the authenticated user + """ + user: User +} + +""" +Forum Thread +""" +type Thread { + """ + The id of the thread + """ + id: Int! + + """ + The title of the thread + """ + title: String + + """ + The text body of the thread (Markdown) + """ + body("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The id of the thread owner user + """ + userId: Int! + + """ + The id of the user who most recently commented on the thread + """ + replyUserId: Int + + """ + The id of the most recent comment on the thread + """ + replyCommentId: Int + + """ + The number of comments on the thread + """ + replyCount: Int + + """ + The number of times users have viewed the thread + """ + viewCount: Int + + """ + If the thread is locked and can receive comments + """ + isLocked: Boolean + + """ + If the thread is stickied and should be displayed at the top of the page + """ + isSticky: Boolean + + """ + If the currently authenticated user is subscribed to the thread + """ + isSubscribed: Boolean + + """ + The amount of likes the thread has + """ + likeCount: Int! + + """ + If the currently authenticated user liked the thread + """ + isLiked: Boolean + + """ + The time of the last reply + """ + repliedAt: Int + + """ + The time of the thread creation + """ + createdAt: Int! + + """ + The time of the thread last update + """ + updatedAt: Int! + + """ + The owner of the thread + """ + user: User + + """ + The user to last reply to the thread + """ + replyUser: User + + """ + The users who liked the thread + """ + likes: [User] + + """ + The url for the thread page on the AniList website + """ + siteUrl: String + + """ + The categories of the thread + """ + categories: [ThreadCategory] + + """ + The media categories of the thread + """ + mediaCategories: [Media] +} + +""" +A forum thread category +""" +type ThreadCategory { + """ + The id of the category + """ + id: Int! + + """ + The name of the category + """ + name: String! +} + +""" +Forum Thread Comment +""" +type ThreadComment { + """ + The id of the comment + """ + id: Int! + + """ + The user id of the comment's owner + """ + userId: Int + + """ + The id of thread the comment belongs to + """ + threadId: Int + + """ + The text content of the comment (Markdown) + """ + comment("Return the string in pre-parsed html instead of markdown" asHtml: Boolean): String + + """ + The amount of likes the comment has + """ + likeCount: Int! + + """ + If the currently authenticated user liked the comment + """ + isLiked: Boolean + + """ + The url for the comment page on the AniList website + """ + siteUrl: String + + """ + The time of the comments creation + """ + createdAt: Int! + + """ + The time of the comments last update + """ + updatedAt: Int! + + """ + The thread the comment belongs to + """ + thread: Thread + + """ + The user who created the comment + """ + user: User + + """ + The users who liked the comment + """ + likes: [User] + + """ + The comment's child reply comments + """ + childComments: Json + + """ + If the comment tree is locked and may not receive replies or edits + """ + isLocked: Boolean +} + +""" +Notification for when a user replies to your forum thread comment +""" +type ThreadCommentReplyNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who create the comment reply + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the reply comment + """ + commentId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The thread that the relevant comment belongs to + """ + thread: Thread + + """ + The reply thread comment + """ + comment: ThreadComment + + """ + The user who replied to the activity + """ + user: User +} + +""" +Notification for when a user replies to a subscribed forum thread +""" +type ThreadCommentSubscribedNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who commented on the thread + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the new comment in the subscribed thread + """ + commentId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The thread that the relevant comment belongs to + """ + thread: Thread + + """ + The reply thread comment + """ + comment: ThreadComment + + """ + The user who replied to the subscribed thread + """ + user: User +} + +""" +Notification for when a thread comment is liked +""" +type ThreadCommentLikeNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who liked to the activity + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the activity which was liked + """ + commentId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The thread that the relevant comment belongs to + """ + thread: Thread + + """ + The thread comment that was liked + """ + comment: ThreadComment + + """ + The user who liked the activity + """ + user: User +} + +""" +Notification for when a thread is liked +""" +type ThreadLikeNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The id of the user who liked to the activity + """ + userId: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the thread which was liked + """ + threadId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The thread that the relevant comment belongs to + """ + thread: Thread + + """ + The liked thread comment + """ + comment: ThreadComment + + """ + The user who liked the activity + """ + user: User +} + +""" +Notification for when new media is added to the site +""" +type RelatedMediaAdditionNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the new media + """ + mediaId: Int! + + """ + The notification context text + """ + context: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The associated media of the airing schedule + """ + media: Media +} + +""" +Notification for when a media entry's data was changed in a significant way impacting users' list tracking +""" +type MediaDataChangeNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the media that received data changes + """ + mediaId: Int! + + """ + The reason for the media data change + """ + context: String + + """ + The reason for the media data change + """ + reason: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The media that received data changes + """ + media: Media +} + +""" +Notification for when a media entry is merged into another for a user who had it on their list +""" +type MediaMergeNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The id of the media that was merged into + """ + mediaId: Int! + + """ + The title of the deleted media + """ + deletedMediaTitles: [String] + + """ + The reason for the media data change + """ + context: String + + """ + The reason for the media merge + """ + reason: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The media that was merged into + """ + media: Media +} + +""" +Notification for when a media tracked in a user's list is deleted from the site +""" +type MediaDeletionNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The title of the deleted media + """ + deletedMediaTitle: String + + """ + The reason for the media deletion + """ + context: String + + """ + The reason for the media deletion + """ + reason: String + + """ + The time the notification was created at + """ + createdAt: Int +} + +""" +Notification for when a media submission is accepted, partially accepted, or rejected +""" +type MediaSubmissionUpdateNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The notification context text + """ + contexts: [String] + + """ + The status of the submission + """ + status: String + + """ + The notes of the submission + """ + notes: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The media that was created or modified. If this submission was to create a new media and it was rejected, this will be null. + """ + media: Media + + """ + The title of the media that was submitted. If this submission was to edit an existing media, this will be null. + """ + submittedTitle: String +} + +""" +Notification for when a staff submission is accepted, partially accepted, or rejected +""" +type StaffSubmissionUpdateNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The notification context text + """ + contexts: [String] + + """ + The status of the submission + """ + status: String + + """ + The notes of the submission + """ + notes: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The staff that was modified. + """ + staff: Staff +} + +""" +Notification for when a character submission is accepted, partially accepted, or rejected +""" +type CharacterSubmissionUpdateNotification { + """ + The id of the Notification + """ + id: Int! + + """ + The type of notification + """ + type: NotificationType + + """ + The notification context text + """ + contexts: [String] + + """ + The status of the submission + """ + status: String + + """ + The notes of the submission + """ + notes: String + + """ + The time the notification was created at + """ + createdAt: Int + + """ + The character that was modified. + """ + character: Character +} + +""" +Activity sort enums +""" +enum ActivitySort { + ID + + ID_DESC + + PINNED +} + +""" +Thread sort enums +""" +enum ThreadSort { + ID + + ID_DESC + + TITLE + + TITLE_DESC + + CREATED_AT @deprecated(reason: "Use ID instead") + + CREATED_AT_DESC @deprecated(reason: "Use ID_DESC instead") + + UPDATED_AT + + UPDATED_AT_DESC + + REPLIED_AT + + REPLIED_AT_DESC + + REPLY_COUNT + + REPLY_COUNT_DESC + + VIEW_COUNT + + VIEW_COUNT_DESC + + IS_STICKY + + SEARCH_MATCH +} + +""" +Thread comments sort enums +""" +enum ThreadCommentSort { + ID + + ID_DESC +} + +""" +Types that can be liked +""" +enum LikeableType { + THREAD + + THREAD_COMMENT + + ACTIVITY + + ACTIVITY_REPLY +} + +""" +List of anime or manga +""" +type MediaListCollection { + """ + Grouped media list entries + """ + lists: [MediaListGroup] + + """ + The owner of the list + """ + user: User + + """ + If there is another chunk + """ + hasNextChunk: Boolean + + """ + A map of media list entry arrays grouped by status + """ + statusLists(asArray: Boolean): [[MediaList]] @deprecated(reason: "Not GraphQL spec compliant, use lists field instead.") + + """ + A map of media list entry arrays grouped by custom lists + """ + customLists(asArray: Boolean): [[MediaList]] @deprecated(reason: "Not GraphQL spec compliant, use lists field instead.") +} + +""" +List group of anime or manga entries +""" +type MediaListGroup { + """ + Media list entries + """ + entries: [MediaList] + + name: String + + isCustomList: Boolean + + isSplitCompletedList: Boolean + + status: MediaListStatus +} + +""" +Provides the parsed markdown as html +""" +type ParsedMarkdown { + """ + The parsed markdown as html + """ + html: String +} + +type AniChartUser { + user: User + + settings: Json + + highlights: Json +} + +type SiteStatistics { + users(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection + + anime(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection + + manga(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection + + characters(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection + + staff(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection + + studios(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection + + reviews(sort: [SiteTrendSort], "The page" page: Int, "The amount of entries per page, max 25" perPage: Int): SiteTrendConnection +} + +""" +Site trend sort enums +""" +enum SiteTrendSort { + DATE + + DATE_DESC + + COUNT + + COUNT_DESC + + CHANGE + + CHANGE_DESC +} + +type SiteTrendConnection { + edges: [SiteTrendEdge] + + nodes: [SiteTrend] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +Site trend connection edge +""" +type SiteTrendEdge { + node: SiteTrend +} + +""" +Daily site statistics +""" +type SiteTrend { + """ + The day the data was recorded (timestamp) + """ + date: Int! + + count: Int! + + """ + The change from yesterday + """ + change: Int! +} + +enum ExternalLinkMediaType { + ANIME + + MANGA + + STAFF +} + +type Mutation { + UpdateUser("User's about/bio text" about: String, "User's title language" titleLanguage: UserTitleLanguage, "If the user should see media marked as adult-only" displayAdultContent: Boolean, "If the user should get notifications when a show they are watching aires" airingNotifications: Boolean, "The user's list scoring system" scoreFormat: ScoreFormat, "The user's default list order" rowOrder: String, "Profile highlight color" profileColor: String, "Profile highlight color (Max: 24)" donatorBadge: String, "Notification options" notificationOptions: [NotificationOptionInput], "Timezone offset format: -?HH:MM" timezone: String, "Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. (Min: 0)" activityMergeTime: Int, "The user's anime list options" animeListOptions: MediaListOptionsInput, "The user's anime list options" mangaListOptions: MediaListOptionsInput, "The language the user wants to see staff and character names in" staffNameLanguage: UserStaffNameLanguage, "Only allow messages from other users the user follows" restrictMessagesToFollowing: Boolean, disabledListActivity: [ListActivityOptionInput]): User + + """ + Create or update a media list entry + """ + SaveMediaListEntry("The list entry id, required for updating" id: Int, "The id of the media the entry is of" mediaId: Int, "The watching/reading status" status: MediaListStatus, "The score of the media in the user's chosen scoring method (Min: 0, Max: 100)" score: Float, "The score of the media in 100 point (Min: 0, Max: 100)" scoreRaw: Int, "The amount of episodes/chapters consumed by the user (Min: 0)" progress: Int, "The amount of volumes read by the user (Min: 0)" progressVolumes: Int, "The amount of times the user has rewatched/read the media (Min: 0, Max: 1000)" repeat: Int, "Priority of planning (Min: 0, Max: 255)" priority: Int, "If the entry should only be visible to authenticated user" private: Boolean, "Text notes (Min: 0, Max: 6000)" notes: String, "If the entry shown be hidden from non-custom lists" hiddenFromStatusLists: Boolean, "Array of custom list names which should be enabled for this entry" customLists: [String], "Array of advanced scores (Min: 0, Max: 100)" advancedScores: [Float], "When the entry was started by the user" startedAt: FuzzyDateInput, "When the entry was completed by the user" completedAt: FuzzyDateInput): MediaList + + """ + Update multiple media list entries to the same values + """ + UpdateMediaListEntries("The watching/reading status" status: MediaListStatus, "The score of the media in the user's chosen scoring method (Min: 0, Max: 100)" score: Float, "The score of the media in 100 point (Min: 0, Max: 100)" scoreRaw: Int, "The amount of episodes/chapters consumed by the user (Min: 0)" progress: Int, "The amount of volumes read by the user (Min: 0)" progressVolumes: Int, "The amount of times the user has rewatched/read the media (Min: 0, Max: 1000)" repeat: Int, "Priority of planning (Min: 0, Max: 255)" priority: Int, "If the entry should only be visible to authenticated user" private: Boolean, "Text notes (Min: 0, Max: 6000)" notes: String, "If the entry shown be hidden from non-custom lists" hiddenFromStatusLists: Boolean, "Array of advanced scores (Min: 0, Max: 100)" advancedScores: [Float], "When the entry was started by the user" startedAt: FuzzyDateInput, "When the entry was completed by the user" completedAt: FuzzyDateInput, "The list entries ids to update" ids: [Int]): [MediaList] + + """ + Delete a media list entry + """ + DeleteMediaListEntry("The id of the media list entry to delete" id: Int): Deleted + + """ + Delete a custom list and remove the list entries from it + """ + DeleteCustomList("The name of the custom list to delete" customList: String, "The media list type of the custom list" type: MediaType): Deleted + + """ + Create or update text activity for the currently authenticated user + """ + SaveTextActivity("The activity's id, required for updating" id: Int, "The activity text (Min: 5, Max: 10000)" text: String, "If the activity should be locked. (Mod Only)" locked: Boolean): TextActivity + + """ + Create or update message activity for the currently authenticated user + """ + SaveMessageActivity("The activity id, required for updating" id: Int, "The activity message text (Min: 2, Max: 10000)" message: String, "The id of the user the message is being sent to" recipientId: Int, "If the activity should be private" private: Boolean, "If the activity should be locked. (Mod Only)" locked: Boolean, "If the message should be sent from the Moderator account (Mod Only)" asMod: Boolean): MessageActivity + + """ + Update list activity (Mod Only) + """ + SaveListActivity("The activity's id, required for updating" id: Int, "If the activity should be locked. (Mod Only)" locked: Boolean): ListActivity + + """ + Delete an activity item of the authenticated users + """ + DeleteActivity("The id of the activity to delete" id: Int): Deleted + + """ + Toggle activity to be pinned to the top of the user's activity feed + """ + ToggleActivityPin("Toggle activity id to be pinned" id: Int, "If the activity should be pinned or unpinned" pinned: Boolean): ActivityUnion + + """ + Toggle the subscription of an activity item + """ + ToggleActivitySubscription("The id of the activity to un/subscribe" activityId: Int, "Whether to subscribe or unsubscribe from the activity" subscribe: Boolean): ActivityUnion + + """ + Create or update an activity reply + """ + SaveActivityReply("The activity reply id, required for updating" id: Int, "The id of the parent activity being replied to" activityId: Int, "The reply text (Min: 2, Max: 8000)" text: String, "If the reply should be sent from the Moderator account (Mod Only)" asMod: Boolean): ActivityReply + + """ + Delete an activity reply of the authenticated users + """ + DeleteActivityReply("The id of the reply to delete" id: Int): Deleted + + """ + Add or remove a like from a likeable type. + Returns all the users who liked the same model + """ + ToggleLike("The id of the likeable type" id: Int, "The type of model to be un/liked" type: LikeableType): [User] + + """ + Add or remove a like from a likeable type. + """ + ToggleLikeV2("The id of the likeable type" id: Int, "The type of model to be un/liked" type: LikeableType): LikeableUnion + + """ + Toggle the un/following of a user + """ + ToggleFollow("The id of the user to un/follow" userId: Int): User + + """ + Favourite or unfavourite an anime, manga, character, staff member, or studio + """ + ToggleFavourite("The id of the anime to un/favourite" animeId: Int, "The id of the manga to un/favourite" mangaId: Int, "The id of the character to un/favourite" characterId: Int, "The id of the staff to un/favourite" staffId: Int, "The id of the studio to un/favourite" studioId: Int): Favourites + + """ + Update the order favourites are displayed in + """ + UpdateFavouriteOrder("The id of the anime to un/favourite" animeIds: [Int], "The id of the manga to un/favourite" mangaIds: [Int], "The id of the character to un/favourite" characterIds: [Int], "The id of the staff to un/favourite" staffIds: [Int], "The id of the studio to un/favourite" studioIds: [Int], "List of integers which the anime should be ordered by (Asc)" animeOrder: [Int], "List of integers which the manga should be ordered by (Asc)" mangaOrder: [Int], "List of integers which the character should be ordered by (Asc)" characterOrder: [Int], "List of integers which the staff should be ordered by (Asc)" staffOrder: [Int], "List of integers which the studio should be ordered by (Asc)" studioOrder: [Int]): Favourites + + """ + Create or update a review + """ + SaveReview("The review id, required for updating" id: Int, "The id of the media the review is of" mediaId: Int, "The main review text (Min: 2600)" body: String, "A short summary/preview of the review (Min: 20, Max: 120)" summary: String, "The score of the review (Min: 0, Max: 100)" score: Int, "If the review should only be visible to its creator" private: Boolean): Review + + """ + Delete a review + """ + DeleteReview("The id of the review to delete" id: Int): Deleted + + """ + Rate a review + """ + RateReview("The id of the review to rate" reviewId: Int, "The rating to apply to the review" rating: ReviewRating): Review + + """ + Recommendation a media + """ + SaveRecommendation("The id of the base media" mediaId: Int, "The id of the media to recommend" mediaRecommendationId: Int, "The rating to give the recommendation" rating: RecommendationRating): Recommendation + + """ + Create or update a forum thread + """ + SaveThread("The thread id, required for updating" id: Int, "The title of the thread (Min: 6, Max: 120)" title: String, "The main text body of the thread (Max: 30000)" body: String, "Forum categories the thread should be within" categories: [Int], "Media related to the contents of the thread" mediaCategories: [Int], "If the thread should be stickied. (Mod Only)" sticky: Boolean, "If the thread should be locked. (Mod Only)" locked: Boolean): Thread + + """ + Delete a thread + """ + DeleteThread("The id of the thread to delete" id: Int): Deleted + + """ + Toggle the subscription of a forum thread + """ + ToggleThreadSubscription("The id of the forum thread to un/subscribe" threadId: Int, "Whether to subscribe or unsubscribe from the forum thread" subscribe: Boolean): Thread + + """ + Create or update a thread comment + """ + SaveThreadComment("The comment id, required for updating" id: Int, "The id of thread the comment belongs to" threadId: Int, "The id of thread comment to reply to" parentCommentId: Int, "The comment markdown text (Min: 1, Max: 12000)" comment: String, "If the comment tree should be locked. (Mod Only)" locked: Boolean): ThreadComment + + """ + Delete a thread comment + """ + DeleteThreadComment("The id of the thread comment to delete" id: Int): Deleted + + UpdateAniChartSettings(titleLanguage: String, outgoingLinkProvider: String, theme: String, sort: String): Json + + UpdateAniChartHighlights(highlights: [AniChartHighlightInput]): Json +} + +""" +Notification option input +""" +input NotificationOptionInput { + """ + The type of notification + """ + type: NotificationType + + """ + Whether this type of notification is enabled + """ + enabled: Boolean +} + +""" +A user's list options for anime or manga lists +""" +input MediaListOptionsInput { + """ + The order each list should be displayed in + """ + sectionOrder: [String] + + """ + If the completed sections of the list should be separated by format + """ + splitCompletedSectionByFormat: Boolean + + """ + The names of the user's custom lists + """ + customLists: [String] + + """ + The names of the user's advanced scoring sections + """ + advancedScoring: [String] + + """ + If advanced scoring is enabled + """ + advancedScoringEnabled: Boolean + + """ + list theme + """ + theme: String +} + +input ListActivityOptionInput { + disabled: Boolean + + type: MediaListStatus +} + +""" +Date object that allows for incomplete date values (fuzzy) +""" +input FuzzyDateInput { + """ + Numeric Year (2017) + """ + year: Int + + """ + Numeric Month (3) + """ + month: Int + + """ + Numeric Day (24) + """ + day: Int +} + +""" +Deleted data type +""" +type Deleted { + """ + If an item has been successfully deleted + """ + deleted: Boolean +} + +""" +Likeable union type +""" +union LikeableUnion = ListActivity|TextActivity|MessageActivity|ActivityReply|Thread|ThreadComment + +input AniChartHighlightInput { + mediaId: Int + + highlight: String +} + +""" +Page of data. Limited to a max depth of 5000 entries. This is calculated as the page parameter multiplied by the perPage parameter. +""" +type InternalPage { + mediaSubmissions(mediaId: Int, submissionId: Int, userId: Int, assigneeId: Int, status: SubmissionStatus, "Filter by the media's type" type: MediaType, "The order the results will be returned in" sort: [SubmissionSort]): [MediaSubmission] + + characterSubmissions(characterId: Int, "Filter by the submitter of the submission" userId: Int, assigneeId: Int, "Filter by the status of the submission" status: SubmissionStatus, "The order the results will be returned in" sort: [SubmissionSort]): [CharacterSubmission] + + staffSubmissions(staffId: Int, "Filter by the submitter of the submission" userId: Int, assigneeId: Int, "Filter by the status of the submission" status: SubmissionStatus, "The order the results will be returned in" sort: [SubmissionSort]): [StaffSubmission] + + revisionHistory("Filter by the user id" userId: Int, "Filter by the media id" mediaId: Int, "Filter by the character id" characterId: Int, "Filter by the staff id" staffId: Int, "Filter by the studio id" studioId: Int): [RevisionHistory] + + reports(reporterId: Int, reportedId: Int): [Report] + + modActions(userId: Int, modId: Int, modId_not: Int, "(max 10,000 items)" modId_in: [Int], "(max 10,000 items)" modId_not_in: [Int]): [ModAction] + + userBlockSearch("Filter by search query" search: String): [User] + + """ + The pagination information + """ + pageInfo: PageInfo + + users("Filter by the user id" id: Int, "Filter by the name of the user" name: String, "Filter to moderators only if true" isModerator: Boolean, "Filter by search query" search: String, "The order the results will be returned in" sort: [UserSort]): [User] + + media("Filter by the media id" id: Int, "Filter by the media's MyAnimeList id" idMal: Int, "Filter by the start date of the media" startDate: FuzzyDateInt, "Filter by the end date of the media" endDate: FuzzyDateInt, "Filter by the season the media was released in" season: MediaSeason, "The year of the season (Winter 2017 would also include December 2016 releases). Requires season argument" seasonYear: Int, "Filter by the media's type" type: MediaType, "Filter by the media's format" format: MediaFormat, "Filter by the media's current release status" status: MediaStatus, "Filter by amount of episodes the media has" episodes: Int, "Filter by the media's episode length" duration: Int, "Filter by the media's chapter count" chapters: Int, "Filter by the media's volume count" volumes: Int, "Filter by if the media's intended for 18+ adult audiences" isAdult: Boolean, "Filter by the media's genres" genre: String, "Filter by the media's tags" tag: String, "Only apply the tags filter argument to tags above this rank. Default: 18" minimumTagRank: Int, "Filter by the media's tags with in a tag category" tagCategory: String, "Filter by the media on the authenticated user's lists" onList: Boolean, "Filter media by sites name with a online streaming or reading license" licensedBy: String, "Filter media by sites id with a online streaming or reading license" licensedById: Int, "Filter by the media's average score" averageScore: Int, "Filter by the number of users with this media on their list" popularity: Int, "Filter by the source type of the media" source: MediaSource, "Filter by the media's country of origin" countryOfOrigin: CountryCode, "If the media is officially licensed or a self-published doujin release" isLicensed: Boolean, "Filter by search query" search: String, "Filter by the media id" id_not: Int, "Filter by the media id (max 10,000 items)" id_in: [Int], "Filter by the media id (max 10,000 items)" id_not_in: [Int], "Filter by the media's MyAnimeList id" idMal_not: Int, "Filter by the media's MyAnimeList id (max 10,000 items)" idMal_in: [Int], "Filter by the media's MyAnimeList id (max 10,000 items)" idMal_not_in: [Int], "Filter by the start date of the media" startDate_greater: FuzzyDateInt, "Filter by the start date of the media" startDate_lesser: FuzzyDateInt, "Filter by the start date of the media" startDate_like: String, "Filter by the end date of the media" endDate_greater: FuzzyDateInt, "Filter by the end date of the media" endDate_lesser: FuzzyDateInt, "Filter by the end date of the media" endDate_like: String, "Filter by the media's format (max 10,000 items)" format_in: [MediaFormat], "Filter by the media's format" format_not: MediaFormat, "Filter by the media's format (max 10,000 items)" format_not_in: [MediaFormat], "Filter by the media's current release status (max 10,000 items)" status_in: [MediaStatus], "Filter by the media's current release status" status_not: MediaStatus, "Filter by the media's current release status (max 10,000 items)" status_not_in: [MediaStatus], "Filter by amount of episodes the media has" episodes_greater: Int, "Filter by amount of episodes the media has" episodes_lesser: Int, "Filter by the media's episode length" duration_greater: Int, "Filter by the media's episode length" duration_lesser: Int, "Filter by the media's chapter count" chapters_greater: Int, "Filter by the media's chapter count" chapters_lesser: Int, "Filter by the media's volume count" volumes_greater: Int, "Filter by the media's volume count" volumes_lesser: Int, "Filter by the media's genres (max 10,000 items)" genre_in: [String], "Filter by the media's genres (max 10,000 items)" genre_not_in: [String], "Filter by the media's tags (max 10,000 items)" tag_in: [String], "Filter by the media's tags (max 10,000 items)" tag_not_in: [String], "Filter by the media's tags with in a tag category (max 10,000 items)" tagCategory_in: [String], "Filter by the media's tags with in a tag category (max 10,000 items)" tagCategory_not_in: [String], "Filter media by sites name with a online streaming or reading license (max 10,000 items)" licensedBy_in: [String], "Filter media by sites id with a online streaming or reading license (max 10,000 items)" licensedById_in: [Int], "Filter by the media's average score" averageScore_not: Int, "Filter by the media's average score" averageScore_greater: Int, "Filter by the media's average score" averageScore_lesser: Int, "Filter by the number of users with this media on their list" popularity_not: Int, "Filter by the number of users with this media on their list" popularity_greater: Int, "Filter by the number of users with this media on their list" popularity_lesser: Int, "Filter by the source type of the media (max 10,000 items)" source_in: [MediaSource], "Filter by the media's country of origin (max 10,000 items)" countryOfOrigin_in: [CountryCode], "Filter by the media's country of origin (max 10,000 items)" countryOfOrigin_not_in: [CountryCode], "The order the results will be returned in" sort: [MediaSort]): [Media] + + characters("Filter by character id" id: Int, "Filter by character by if its their birthday today" isBirthday: Boolean, "Filter by search query" search: String, "Filter by character id" id_not: Int, "Filter by character id (max 10,000 items)" id_in: [Int], "Filter by character id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [CharacterSort]): [Character] + + staff("Filter by the staff id" id: Int, "Filter by staff by if its their birthday today" isBirthday: Boolean, "Filter by search query" search: String, "Filter by the staff id" id_not: Int, "Filter by the staff id (max 10,000 items)" id_in: [Int], "Filter by the staff id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [StaffSort]): [Staff] + + studios("Filter by the studio id" id: Int, "Filter by search query" search: String, "Filter by the studio id" id_not: Int, "Filter by the studio id (max 10,000 items)" id_in: [Int], "Filter by the studio id (max 10,000 items)" id_not_in: [Int], "The order the results will be returned in" sort: [StudioSort]): [Studio] + + mediaList("Filter by a list entry's id" id: Int, "Filter by a user's id" userId: Int, "Filter by a user's name" userName: String, "Filter by the list entries media type" type: MediaType, "Filter by the watching/reading status" status: MediaListStatus, "Filter by the media id of the list entry" mediaId: Int, "Filter list entries to users who are being followed by the authenticated user" isFollowing: Boolean, "Filter by note words and #tags" notes: String, "Filter by the date the user started the media" startedAt: FuzzyDateInt, "Filter by the date the user completed the media" completedAt: FuzzyDateInt, "Limit to only entries also on the auth user's list. Requires user id or name arguments." compareWithAuthList: Boolean, "Filter by a user's id (max 10,000 items)" userId_in: [Int], "Filter by the watching/reading status (max 10,000 items)" status_in: [MediaListStatus], "Filter by the watching/reading status (max 10,000 items)" status_not_in: [MediaListStatus], "Filter by the watching/reading status" status_not: MediaListStatus, "Filter by the media id of the list entry (max 10,000 items)" mediaId_in: [Int], "Filter by the media id of the list entry (max 10,000 items)" mediaId_not_in: [Int], "Filter by note words and #tags" notes_like: String, "Filter by the date the user started the media" startedAt_greater: FuzzyDateInt, "Filter by the date the user started the media" startedAt_lesser: FuzzyDateInt, "Filter by the date the user started the media" startedAt_like: String, "Filter by the date the user completed the media" completedAt_greater: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_lesser: FuzzyDateInt, "Filter by the date the user completed the media" completedAt_like: String, "The order the results will be returned in" sort: [MediaListSort]): [MediaList] + + airingSchedules("Filter by the id of the airing schedule item" id: Int, "Filter by the id of associated media" mediaId: Int, "Filter by the airing episode number" episode: Int, "Filter by the time of airing" airingAt: Int, "Filter to episodes that haven't yet aired" notYetAired: Boolean, "Filter by the id of the airing schedule item" id_not: Int, "Filter by the id of the airing schedule item (max 10,000 items)" id_in: [Int], "Filter by the id of the airing schedule item (max 10,000 items)" id_not_in: [Int], "Filter by the id of associated media" mediaId_not: Int, "Filter by the id of associated media (max 10,000 items)" mediaId_in: [Int], "Filter by the id of associated media (max 10,000 items)" mediaId_not_in: [Int], "Filter by the airing episode number" episode_not: Int, "Filter by the airing episode number (max 10,000 items)" episode_in: [Int], "Filter by the airing episode number (max 10,000 items)" episode_not_in: [Int], "Filter by the airing episode number" episode_greater: Int, "Filter by the airing episode number" episode_lesser: Int, "Filter by the time of airing" airingAt_greater: Int, "Filter by the time of airing" airingAt_lesser: Int, "The order the results will be returned in" sort: [AiringSort]): [AiringSchedule] + + mediaTrends("Filter by the media id" mediaId: Int, "Filter by date" date: Int, "Filter by trending amount" trending: Int, "Filter by score" averageScore: Int, "Filter by popularity" popularity: Int, "Filter by episode number" episode: Int, "Filter to stats recorded while the media was releasing" releasing: Boolean, "Filter by the media id" mediaId_not: Int, "Filter by the media id (max 10,000 items)" mediaId_in: [Int], "Filter by the media id (max 10,000 items)" mediaId_not_in: [Int], "Filter by date" date_greater: Int, "Filter by date" date_lesser: Int, "Filter by trending amount" trending_greater: Int, "Filter by trending amount" trending_lesser: Int, "Filter by trending amount" trending_not: Int, "Filter by score" averageScore_greater: Int, "Filter by score" averageScore_lesser: Int, "Filter by score" averageScore_not: Int, "Filter by popularity" popularity_greater: Int, "Filter by popularity" popularity_lesser: Int, "Filter by popularity" popularity_not: Int, "Filter by episode number" episode_greater: Int, "Filter by episode number" episode_lesser: Int, "Filter by episode number" episode_not: Int, "The order the results will be returned in" sort: [MediaTrendSort]): [MediaTrend] + + notifications("Filter by the type of notifications" type: NotificationType, "Reset the unread notification count to 0 on load" resetNotificationCount: Boolean, "Filter by the type of notifications (max 10,000 items)" type_in: [NotificationType]): [NotificationUnion] + + followers("User id of the follower/followed" userId: Int!, "The order the results will be returned in" sort: [UserSort]): [User] + + following("User id of the follower/followed" userId: Int!, "The order the results will be returned in" sort: [UserSort]): [User] + + activities("Filter by the activity id" id: Int, "Filter by the owner user id" userId: Int, "Filter by the id of the user who sent a message" messengerId: Int, "Filter by the associated media id of the activity" mediaId: Int, "Filter by the type of activity" type: ActivityType, "Filter activity to users who are being followed by the authenticated user" isFollowing: Boolean, "Filter activity to only activity with replies" hasReplies: Boolean, "Filter activity to only activity with replies or is of type text" hasRepliesOrTypeText: Boolean, "Filter by the time the activity was created" createdAt: Int, "Filter by the activity id" id_not: Int, "Filter by the activity id (max 10,000 items)" id_in: [Int], "Filter by the activity id (max 10,000 items)" id_not_in: [Int], "Filter by the owner user id" userId_not: Int, "Filter by the owner user id (max 10,000 items)" userId_in: [Int], "Filter by the owner user id (max 10,000 items)" userId_not_in: [Int], "Filter by the id of the user who sent a message" messengerId_not: Int, "Filter by the id of the user who sent a message (max 10,000 items)" messengerId_in: [Int], "Filter by the id of the user who sent a message (max 10,000 items)" messengerId_not_in: [Int], "Filter by the associated media id of the activity" mediaId_not: Int, "Filter by the associated media id of the activity (max 10,000 items)" mediaId_in: [Int], "Filter by the associated media id of the activity (max 10,000 items)" mediaId_not_in: [Int], "Filter by the type of activity" type_not: ActivityType, "Filter by the type of activity (max 10,000 items)" type_in: [ActivityType], "Filter by the type of activity (max 10,000 items)" type_not_in: [ActivityType], "Filter by the time the activity was created" createdAt_greater: Int, "Filter by the time the activity was created" createdAt_lesser: Int, "The order the results will be returned in" sort: [ActivitySort]): [ActivityUnion] + + activityReplies("Filter by the reply id" id: Int, "Filter by the parent id" activityId: Int): [ActivityReply] + + threads("Filter by the thread id" id: Int, "Filter by the user id of the thread's creator" userId: Int, "Filter by the user id of the last user to comment on the thread" replyUserId: Int, "Filter by if the currently authenticated user's subscribed threads" subscribed: Boolean, "Filter by thread category id" categoryId: Int, "Filter by thread media id category" mediaCategoryId: Int, "Filter by search query" search: String, "Filter by the thread id (max 10,000 items)" id_in: [Int], "The order the results will be returned in" sort: [ThreadSort]): [Thread] + + threadComments("Filter by the comment id" id: Int, "Filter by the thread id" threadId: Int, "Filter by the user id of the comment's creator" userId: Int, "The order the results will be returned in" sort: [ThreadCommentSort]): [ThreadComment] + + reviews("Filter by Review id" id: Int, "Filter by media id" mediaId: Int, "Filter by user id" userId: Int, "Filter by media type" mediaType: MediaType, "The order the results will be returned in" sort: [ReviewSort]): [Review] + + recommendations("Filter by recommendation id" id: Int, "Filter by media id" mediaId: Int, "Filter by media recommendation id" mediaRecommendationId: Int, "Filter by user who created the recommendation" userId: Int, "Filter by total rating of the recommendation" rating: Int, "Filter by the media on the authenticated user's lists" onList: Boolean, "Filter by total rating of the recommendation" rating_greater: Int, "Filter by total rating of the recommendation" rating_lesser: Int, "The order the results will be returned in" sort: [RecommendationSort]): [Recommendation] + + likes("The id of the likeable type" likeableId: Int, "The type of model the id applies to" type: LikeableType): [User] +} + +""" +Submission status +""" +enum SubmissionStatus { + PENDING + + REJECTED + + PARTIALLY_ACCEPTED + + ACCEPTED +} + +""" +Submission sort enums +""" +enum SubmissionSort { + ID + + ID_DESC +} + +""" +Media submission +""" +type MediaSubmission { + """ + The id of the submission + """ + id: Int! + + """ + User submitter of the submission + """ + submitter: User + + """ + Data Mod assigned to handle the submission + """ + assignee: User + + """ + Status of the submission + """ + status: SubmissionStatus + + submitterStats: Json + + notes: String + + source: String + + changes: [String] + + """ + Whether the submission is locked + """ + locked: Boolean + + media: Media + + submission: Media + + characters: [MediaSubmissionComparison] + + staff: [MediaSubmissionComparison] + + studios: [MediaSubmissionComparison] + + relations: [MediaEdge] + + externalLinks: [MediaSubmissionComparison] + + createdAt: Int +} + +""" +Media submission with comparison to current data +""" +type MediaSubmissionComparison { + submission: MediaSubmissionEdge + + character: MediaCharacter + + staff: StaffEdge + + studio: StudioEdge + + externalLink: MediaExternalLink +} + +type MediaSubmissionEdge { + """ + The id of the direct submission + """ + id: Int + + characterRole: CharacterRole + + staffRole: String + + roleNotes: String + + dubGroup: String + + characterName: String + + isMain: Boolean + + character: Character + + characterSubmission: Character + + voiceActor: Staff + + voiceActorSubmission: Staff + + staff: Staff + + staffSubmission: Staff + + studio: Studio + + externalLink: MediaExternalLink + + media: Media +} + +""" +Internal - Media characters separated +""" +type MediaCharacter { + """ + The id of the connection + """ + id: Int + + """ + The characters role in the media + """ + role: CharacterRole + + roleNotes: String + + dubGroup: String + + """ + Media specific character name + """ + characterName: String + + """ + The characters in the media voiced by the parent actor + """ + character: Character + + """ + The voice actor of the character + """ + voiceActor: Staff +} + +""" +A submission for a character that features in an anime or manga +""" +type CharacterSubmission { + """ + The id of the submission + """ + id: Int! + + """ + Character that the submission is referencing + """ + character: Character + + """ + The character submission changes + """ + submission: Character + + """ + Submitter for the submission + """ + submitter: User + + """ + Data Mod assigned to handle the submission + """ + assignee: User + + """ + Status of the submission + """ + status: SubmissionStatus + + """ + Inner details of submission status + """ + notes: String + + source: String + + """ + Whether the submission is locked + """ + locked: Boolean + + createdAt: Int +} + +""" +A submission for a staff that features in an anime or manga +""" +type StaffSubmission { + """ + The id of the submission + """ + id: Int! + + """ + Staff that the submission is referencing + """ + staff: Staff + + """ + The staff submission changes + """ + submission: Staff + + """ + Submitter for the submission + """ + submitter: User + + """ + Data Mod assigned to handle the submission + """ + assignee: User + + """ + Status of the submission + """ + status: SubmissionStatus + + """ + Inner details of submission status + """ + notes: String + + source: String + + """ + Whether the submission is locked + """ + locked: Boolean + + createdAt: Int +} + +""" +Feed of mod edit activity +""" +type RevisionHistory { + """ + The id of the media + """ + id: Int! + + """ + The action taken on the objects + """ + action: RevisionHistoryAction + + """ + A JSON object of the fields that changed + """ + changes: Json + + """ + The user who made the edit to the object + """ + user: User + + """ + The media the mod feed entry references + """ + media: Media + + """ + The character the mod feed entry references + """ + character: Character + + """ + The staff member the mod feed entry references + """ + staff: Staff + + """ + The studio the mod feed entry references + """ + studio: Studio + + """ + The external link source the mod feed entry references + """ + externalLink: MediaExternalLink + + """ + When the mod feed entry was created + """ + createdAt: Int +} + +""" +Revision history actions +""" +enum RevisionHistoryAction { + CREATE + + EDIT +} + +type Report { + id: Int! + + reporter: User + + reported: User + + reason: String + + """ + When the entry data was created + """ + createdAt: Int + + cleared: Boolean +} + +type ModAction { + """ + The id of the action + """ + id: Int! + + user: User + + mod: User + + type: ModActionType + + objectId: Int + + objectType: String + + data: String + + createdAt: Int! +} + +enum ModActionType { + NOTE + + BAN + + DELETE + + EDIT + + EXPIRE + + REPORT + + RESET + + ANON +} + +""" +The official titles of the media in various languages +""" +input MediaTitleInput { + """ + The romanization of the native language title + """ + romaji: String + + """ + The official english title + """ + english: String + + """ + Official title in it's native language + """ + native: String +} + +input AiringScheduleInput { + airingAt: Int + + episode: Int + + timeUntilAiring: Int +} + +""" +An external link to another site related to the media +""" +input MediaExternalLinkInput { + """ + The id of the external link + """ + id: Int! + + """ + The url of the external link + """ + url: String! + + """ + The site location of the external link + """ + site: String! +} + +""" +The names of the character +""" +input CharacterNameInput { + """ + The character's given name + """ + first: String + + """ + The character's middle name + """ + middle: String + + """ + The character's surname + """ + last: String + + """ + The character's full name in their native language + """ + native: String + + """ + Other names the character might be referred by + """ + alternative: [String] + + """ + Other names the character might be referred to as but are spoilers + """ + alternativeSpoiler: [String] +} + +type CharacterSubmissionConnection { + edges: [CharacterSubmissionEdge] + + nodes: [CharacterSubmission] + + """ + The pagination information + """ + pageInfo: PageInfo +} + +""" +CharacterSubmission connection edge +""" +type CharacterSubmissionEdge { + node: CharacterSubmission + + """ + The characters role in the media + """ + role: CharacterRole + + """ + The voice actors of the character + """ + voiceActors: [Staff] + + """ + The submitted voice actors of the character + """ + submittedVoiceActors: [StaffSubmission] +} + +""" +The names of the staff member +""" +input StaffNameInput { + """ + The person's given name + """ + first: String + + """ + The person's middle name + """ + middle: String + + """ + The person's surname + """ + last: String + + """ + The person's full name in their native language + """ + native: String + + """ + Other names the character might be referred by + """ + alternative: [String] +} + +""" +User data for moderators +""" +type UserModData { + alts: [User] + + bans: Json + + ip: Json + + counts: Json + + privacy: Int + + email: String +} + +""" +The `ID` scalar type represents a unique identifier, often used to +refetch an object or as key for a cache. The ID type appears in a JSON +response as a String; however, it is not intended to be human-readable. +When expected as an input type, any string (such as `"4"`) or integer +(such as `4`) input value will be accepted as an ID. +""" +scalar ID + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. +""" +type __Schema { + """ + A list of all types supported by this server. + """ + types: [__Type!]! + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all directives supported by this server. + """ + directives: [__Directive!]! +} + +""" +The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. +""" +type __Type { + kind: __TypeKind! + + name: String + + description: String + + fields(includeDeprecated: Boolean = false): [__Field!] + + interfaces: [__Type!] + + possibleTypes: [__Type!] + + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + + inputFields: [__InputValue!] + + ofType: __Type +} + +""" +An enum describing what kind of type a given `__Type` is. +""" +enum __TypeKind { + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + + """ + Indicates this type is an enum. `enumValues` is a valid field. + """ + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """ + Indicates this type is a list. `ofType` is a valid field. + """ + LIST + + """ + Indicates this type is a non-null. `ofType` is a valid field. + """ + NON_NULL +} + +""" +Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. +""" +type __Field { + name: String! + + description: String + + args: [__InputValue!]! + + type: __Type! + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. +""" +type __InputValue { + name: String! + + description: String + + type: __Type! + + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String +} + +""" +One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. +""" +type __EnumValue { + name: String! + + description: String + + isDeprecated: Boolean! + + deprecationReason: String +} + +""" +A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + +In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor. +""" +type __Directive { + name: String! + + description: String + + locations: [__DirectiveLocation!]! + + args: [__InputValue!]! + + onOperation: Boolean! @deprecated(reason: "Use `locations`.") + + onFragment: Boolean! @deprecated(reason: "Use `locations`.") + + onField: Boolean! @deprecated(reason: "Use `locations`.") +} + +""" +A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a fragment definition. + """ + FRAGMENT_DEFINITION + + """ + Location adjacent to a fragment spread. + """ + FRAGMENT_SPREAD + + """ + Location adjacent to an inline fragment. + """ + INLINE_FRAGMENT + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an input object field definition. + """ + INPUT_FIELD_DEFINITION +} + +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Marks an element of a GraphQL schema as no longer supported. +""" +directive @deprecated("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE + +schema { + query: Query + mutation: Mutation +} + diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuAddLibManga.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuAddLibManga.graphql new file mode 100644 index 000000000..8a23445c8 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuAddLibManga.graphql @@ -0,0 +1,27 @@ +mutation KitsuAddLibManga( + $media_id: ID!, + $status: LibraryEntryStatusEnum!, + $progress: Int!, + $private: Boolean!, + $rating: Int, +) { + libraryEntry { + create( + input: { + mediaId: $media_id, + mediaType: MANGA, + status: $status, + progress: $progress, + private: $private, + rating: $rating, + } + ) { + errors { + message + } + libraryEntry { + id + } + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuDeleteLibEntry.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuDeleteLibEntry.graphql new file mode 100644 index 000000000..f900a376d --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuDeleteLibEntry.graphql @@ -0,0 +1,18 @@ +mutation KitsuDeleteLibEntry( + $library_id: ID!, +) { + libraryEntry { + delete( + input: { + id: $library_id, + } + ) { + errors { + message + } + libraryEntry { + id + } + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuFindLibManga.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuFindLibManga.graphql new file mode 100644 index 000000000..3a8a336c1 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuFindLibManga.graphql @@ -0,0 +1,18 @@ +query KitsuFindLibManga( + $remote_id: ID!, +) { + findMangaById( + id: $remote_id, + ) { + ...MangaFragment + myLibraryEntry { + id + private + progress + rating + status + startedAt + finishedAt + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetCurrentAccount.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetCurrentAccount.graphql new file mode 100644 index 000000000..cc5442893 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetCurrentAccount.graphql @@ -0,0 +1,9 @@ +query KitsuGetCurrentAccount { + currentAccount { + id + ratingSystem + profile { + name + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsById.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsById.graphql new file mode 100644 index 000000000..8577c1aa2 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsById.graphql @@ -0,0 +1,9 @@ +query KitsuGetMangaDetailsById( + $id: ID!, +) { + findMangaById( + id: $id, + ) { + ...MangaFragment + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsBySlug.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsBySlug.graphql new file mode 100644 index 000000000..ba641c7a4 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuGetMangaDetailsBySlug.graphql @@ -0,0 +1,9 @@ +query KitsuGetMangaDetailsBySlug( + $slug: String!, +) { + findMangaBySlug( + slug: $slug, + ) { + ...MangaFragment + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuSearchMangaByTitle.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuSearchMangaByTitle.graphql new file mode 100644 index 000000000..d01eaa50c --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuSearchMangaByTitle.graphql @@ -0,0 +1,12 @@ +query KitsuSearchMangaByTitle( + $query: String!, +) { + searchMangaByTitle( + title: $query, + first: 20, + ) { + nodes { + ...MangaFragment + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/KitsuUpdateLibManga.graphql b/app/src/main/graphql/mihon/graphql/kitsu/KitsuUpdateLibManga.graphql new file mode 100644 index 000000000..895f77284 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/KitsuUpdateLibManga.graphql @@ -0,0 +1,30 @@ +mutation KitsuUpdateLibManga( + $library_id: ID!, + $status: LibraryEntryStatusEnum!, + $progress: Int!, + $private: Boolean!, + $rating: Int, + $startedAt: ISO8601DateTime, + $finishedAt: ISO8601DateTime, +) { + libraryEntry { + update( + input: { + id: $library_id, + status: $status, + progress: $progress, + private: $private, + rating: $rating, + startedAt: $startedAt, + finishedAt: $finishedAt, + } + ) { + errors { + message + } + libraryEntry { + id + } + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/MangaFragment.graphql b/app/src/main/graphql/mihon/graphql/kitsu/MangaFragment.graphql new file mode 100644 index 000000000..ff4e4c55e --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/MangaFragment.graphql @@ -0,0 +1,31 @@ +fragment MangaFragment on Manga { + id + titles { + preferred + } + chapterCount + staff(first: 5) { + nodes { + role + person { + name + } + } + } + posterImage { + views(names: "small") { + name + url + } + original { + name + url + } + } + description(locales: "en") + status + subtype + startDate + slug + averageRating +} diff --git a/app/src/main/graphql/mihon/graphql/kitsu/kitsu.graphqls b/app/src/main/graphql/mihon/graphql/kitsu/kitsu.graphqls new file mode 100644 index 000000000..2293adf36 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/kitsu/kitsu.graphqls @@ -0,0 +1,6617 @@ +""" +A user account on Kitsu +""" +type Account implements WithTimestamps { + """ + The country this user resides in + """ + country: String + + createdAt: ISO8601DateTime! + + """ + The email addresses associated with this account + """ + email: [String!]! + + """ + The features this user has access to + """ + enabledFeatures: [String!]! + + """ + Facebook account linked to the account + """ + facebookId: String + + id: ID! + + """ + Primary language for the account + """ + language: String + + """ + Longest period an account has had a PRO subscription for in seconds + """ + maxProStreak: Int + + """ + The PRO subscription for this account + """ + proSubscription: ProSubscription + + """ + The profile for this account + """ + profile: Profile! + + """ + Media rating system used for the account + """ + ratingSystem: RatingSystemEnum! + + """ + Whether Not Safe For Work content is accessible + """ + sfwFilter: Boolean + + """ + The level of the SFW Filter + """ + sfwFilterPreference: SfwFilterPreferenceEnum + + """ + The site-wide permissions this user has access to + """ + sitePermissions: [SitePermissionEnum!]! + + """ + Time zone of the account + """ + timeZone: String + + """ + Preferred language for media titles + """ + titleLanguagePreference: TitleLanguagePreferenceEnum + + """ + Twitter account linked to the account + """ + twitterId: String + + updatedAt: ISO8601DateTime! +} + +union AccountChangePasswordErrorsUnion = NotAuthenticatedError|NotAuthorizedError|ValidationError + +input AccountChangePasswordInput { + """ + The current, existing password for the account + """ + oldPassword: String! + + """ + The new password to set + """ + newPassword: String! +} + +""" +Autogenerated return type of AccountChangePassword. +""" +type AccountChangePasswordPayload { + errors: [AccountChangePasswordErrorsUnion!] + + result: Account +} + +union AccountCreateErrorsUnion = ValidationError + +input AccountCreateInput { + """ + The name of the user + """ + name: String! + + """ + The email address to reset the password for + """ + email: String! + + """ + The password for the user + """ + password: String! + + """ + An external identity to associate with the account on creation + """ + externalIdentity: AccountExternalIdentityInput +} + +""" +Autogenerated return type of AccountCreate. +""" +type AccountCreatePayload { + errors: [AccountCreateErrorsUnion!] + + result: Account +} + +input AccountExternalIdentityInput { + provider: ExternalIdentityProviderEnum! + + id: String! +} + +type AccountMutations { + """ + Change your Kitsu account password + """ + changePassword(input: AccountChangePasswordInput!): AccountChangePasswordPayload + + """ + Send a password reset email + """ + sendPasswordReset("The email address to reset the password for" email: String!): AccountSendPasswordResetPayload + + """ + Update the account of the current user. + """ + update(input: AccountUpdateInput!): AccountUpdatePayload +} + +""" +Autogenerated return type of AccountSendPasswordReset. +""" +type AccountSendPasswordResetPayload { + email: String! +} + +union AccountUpdateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input AccountUpdateInput { + """ + The SFW Filter setting + """ + sfwFilterPreference: SfwFilterPreferenceEnum + + """ + The country of the user + """ + country: String + + """ + The theme displayed on Kitsu + """ + siteTheme: SiteThemeEnum + + """ + The preferred rating system + """ + ratingSystem: RatingSystemEnum + + """ + How media titles will get visualized + """ + preferredTitleLanguage: TitleLanguagePreferenceEnum + + """ + The time zone of the user + """ + timeZone: String +} + +""" +Autogenerated return type of AccountUpdate. +""" +type AccountUpdatePayload { + errors: [AccountUpdateErrorsUnion!] + + result: Account +} + +enum AgeRatingEnum { + """ + Acceptable for all ages + """ + G + + """ + Parental guidance suggested; should be safe for preteens and older + """ + PG + + """ + Possible lewd or intense themes; should be safe for teens and older + """ + R + + """ + Contains adult content or themes; should only be viewed by adults + """ + R18 +} + +""" +Generic Amount Consumed based on Media +""" +interface AmountConsumed { + """ + Total media completed atleast once. + """ + completed: Int! + + id: ID! + + """ + Total amount of media. + """ + media: Int! + + """ + The profile related to the user for this stat. + """ + profile: Profile! + + """ + Last time we fully recalculated this stat. + """ + recalculatedAt: ISO8601Date! + + """ + Total progress of library including reconsuming. + """ + units: Int! +} + +type Anime implements Episodic & Media & WithTimestamps { + """ + The recommended minimum age group for this media + """ + ageRating: AgeRatingEnum + + """ + An explanation of why this received the age rating it did + """ + ageRatingGuide: String + + """ + The average rating of this media amongst all Kitsu users + """ + averageRating: Float + + """ + The rank of this media by rating + """ + averageRatingRank: Int + + """ + A large banner image for this media + """ + bannerImage: Image + + """ + A list of categories for this media + """ + categories("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaCategorySortOption]): CategoryConnection! + + """ + The characters who starred in this media + """ + characters("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaCharacterSortOption]): MediaCharacterConnection! + + createdAt: ISO8601DateTime! + + """ + A brief (mostly spoiler free) summary or description of the media. + """ + description(locales: [String!]): Map! + + """ + the day that this media made its final release + """ + endDate: Date + + """ + The number of episodes in this series + """ + episodeCount: Int + + """ + The general length (in seconds) of each episode + """ + episodeLength: Int + + """ + Episodes for this media + """ + episodes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, number: [Int!] @deprecated(reason: "This should only grab all episodes. Separate field will be provided for specific episodes"), sort: [EpisodeSortOption]): EpisodeConnection! + + """ + The number of users with this in their favorites + """ + favoritesCount: Int + + id: ID! + + """ + A list of mappings for this media + """ + mappings("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MappingConnection! + + """ + Your library entry related to this media. + """ + myLibraryEntry: LibraryEntry + + """ + A list of your wiki submissions for this media + """ + myWikiSubmissions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [WikiSubmissionSortOption]): WikiSubmissionConnection! + + """ + The time of the next release of this media + """ + nextRelease: ISO8601DateTime + + """ + The countries in which the media was originally primarily produced + """ + originCountries: [String!]! + + """ + The languages the media was originally produced in + """ + originLanguages: [String!]! + + """ + The country in which the media was primarily produced + """ + originalLocale: String @deprecated(reason: "Replaced with originCountries and originLanguages") + + """ + The poster image of this media + """ + posterImage: Image + + """ + All posts that tag this media. + """ + posts("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [PostSortOption]): PostConnection! + + """ + The companies which helped to produce this media + """ + productions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaProductionConnection! + + """ + A list of quotes from this media + """ + quotes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): QuoteConnection! + + """ + A list of reactions for this media + """ + reactions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaReactionSortOption]): MediaReactionConnection! + + """ + A list of relationships for this media + """ + relationships("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaRelationshipConnection! + + """ + The season this was released in + """ + season: ReleaseSeasonEnum + + """ + Whether the media is Safe-for-Work + """ + sfw: Boolean! + + """ + The URL-friendly identifier of this media + """ + slug: String! + + """ + The staff members who worked on this media + """ + staff("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaStaffConnection! + + """ + The day that this media first released + """ + startDate: Date + + """ + The current releasing status of this media + """ + status: ReleaseStatusEnum! + + """ + The stream links. + """ + streamingLinks("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): StreamingLinkConnection! + + """ + A secondary type for categorizing Anime. + """ + subtype: AnimeSubtypeEnum! + + """ + Description of when this media is expected to release + """ + tba: String + + """ + The titles for this media in various locales + """ + titles: TitlesList! + + """ + The total length (in seconds) of the entire series + """ + totalLength: Int + + """ + Anime or Manga. + """ + type: String! + + updatedAt: ISO8601DateTime! + + """ + The number of users with this in their library + """ + userCount: Int + + """ + The rank of this media by popularity + """ + userCountRank: Int + + """ + Video id for a trailer on YouTube + """ + youtubeTrailerVideoId: String +} + +type AnimeAmountConsumed implements AmountConsumed { + """ + Total media completed atleast once. + """ + completed: Int! + + id: ID! + + """ + Total amount of media. + """ + media: Int! + + """ + The profile related to the user for this stat. + """ + profile: Profile! + + """ + Last time we fully recalculated this stat. + """ + recalculatedAt: ISO8601Date! + + """ + Total time spent in minutes. + """ + time: Int! + + """ + Total progress of library including reconsuming. + """ + units: Int! +} + +type AnimeCategoryBreakdown implements CategoryBreakdown { + """ + A Map of category_id -> count for all categories present on the library entries + """ + categories: Map! + + id: ID! + + """ + The profile related to the user for this stat. + """ + profile: Profile! + + """ + Last time we fully recalculated this stat. + """ + recalculatedAt: ISO8601Date! + + """ + The total amount of library entries. + """ + total: Int! +} + +""" +The connection type for Anime. +""" +type AnimeConnection { + """ + A list of edges. + """ + edges: [AnimeEdge] + + """ + A list of nodes. + """ + nodes: [Anime] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +input AnimeCreateInput { + titles: TitlesListInput! + + description: Map! + + ageRating: AgeRatingEnum + + ageRatingGuide: String + + tba: String + + startDate: Date + + endDate: Date + + posterImage: Upload + + bannerImage: Upload + + youtubeTrailerVideoId: String + + episodeCount: Int + + episodeLength: Int +} + +""" +Autogenerated return type of AnimeCreate. +""" +type AnimeCreatePayload { + anime: Anime + + errors: [Error!] +} + +""" +Autogenerated return type of AnimeDelete. +""" +type AnimeDeletePayload { + anime: GenericDelete + + errors: [Error!] +} + +""" +An edge in a connection. +""" +type AnimeEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Anime +} + +type AnimeMutations { + """ + Create an Anime. + """ + create("Create an Anime." input: AnimeCreateInput!): AnimeCreatePayload + + """ + Delete an Anime. + """ + delete("Delete an Anime." input: GenericDeleteInput!): AnimeDeletePayload + + """ + Update an Anime. + """ + update("Update an Anime." input: AnimeUpdateInput!): AnimeUpdatePayload +} + +enum AnimeSubtypeEnum { + TV + + """ + Spinoffs or Extras of the original. + """ + SPECIAL + + """ + Original Video Animation. Anime directly released to video market. + """ + OVA + + """ + Original Net Animation (Web Anime). + """ + ONA + + MOVIE + + MUSIC +} + +input AnimeUpdateInput { + id: ID! + + titles: TitlesListInput + + description: Map + + ageRating: AgeRatingEnum + + ageRatingGuide: String + + tba: String + + startDate: Date + + endDate: Date + + posterImage: Upload + + bannerImage: Upload + + youtubeTrailerVideoId: String + + episodeCount: Int + + episodeLength: Int +} + +""" +Autogenerated return type of AnimeUpdate. +""" +type AnimeUpdatePayload { + anime: Anime + + errors: [Error!] +} + +""" +A blocked user entry of an Account. +""" +type Block implements WithTimestamps { + """ + User who got blocked. + """ + blockedUser: Profile! + + createdAt: ISO8601DateTime! + + id: ID! + + updatedAt: ISO8601DateTime! + + """ + User who blocked. + """ + user: Profile! +} + +""" +The connection type for Block. +""" +type BlockConnection { + """ + A list of edges. + """ + edges: [BlockEdge] + + """ + A list of nodes. + """ + nodes: [Block] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +union BlockCreateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input BlockCreateInput { + """ + The id of the user to block. + """ + blockedId: ID! +} + +""" +Autogenerated return type of BlockCreate. +""" +type BlockCreatePayload { + errors: [BlockCreateErrorsUnion!] + + result: Block +} + +union BlockDeleteErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input BlockDeleteInput { + """ + The id of the block. + """ + blockId: ID! +} + +""" +Autogenerated return type of BlockDelete. +""" +type BlockDeletePayload { + errors: [BlockDeleteErrorsUnion!] + + result: Block +} + +""" +An edge in a connection. +""" +type BlockEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Block +} + +type BlockMutations { + """ + Create a Block entry. + """ + create(input: BlockCreateInput!): BlockCreatePayload + + """ + Delete a Block entry. + """ + delete(input: BlockDeleteInput!): BlockDeletePayload +} + +""" +Represents `true` or `false` values. +""" +scalar Boolean + +""" +Information about a specific Category +""" +type Category implements WithTimestamps { + """ + The child categories. + """ + children("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): CategoryConnection + + createdAt: ISO8601DateTime! + + """ + A brief summary or description of the catgory. + """ + description(locales: [String!]): Map! + + id: ID! + + """ + Whether the category is Not-Safe-for-Work. + """ + isNsfw: Boolean! + + """ + The parent category. Each category can have one parent. + """ + parent: Category + + """ + The top-level ancestor category + """ + root: Category + + """ + The URL-friendly identifier of this Category. + """ + slug: String! + + """ + The name of the category. + """ + title(locales: [String!]): Map! + + updatedAt: ISO8601DateTime! +} + +""" +Generic Category Breakdown based on Media +""" +interface CategoryBreakdown { + """ + A Map of category_id -> count for all categories present on the library entries + """ + categories: Map! + + id: ID! + + """ + The profile related to the user for this stat. + """ + profile: Profile! + + """ + Last time we fully recalculated this stat. + """ + recalculatedAt: ISO8601Date! + + """ + The total amount of library entries. + """ + total: Int! +} + +""" +The connection type for Category. +""" +type CategoryConnection { + """ + A list of edges. + """ + edges: [CategoryEdge] + + """ + A list of nodes. + """ + nodes: [Category] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CategoryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Category +} + +""" +A single chapter of a manga +""" +type Chapter implements Unit & WithTimestamps { + createdAt: ISO8601DateTime! + + """ + A brief summary or description of the unit + """ + description(locales: [String!]): Map! + + id: ID! + + """ + Number of pages in chapter. + """ + length: Int + + """ + The manga this chapter is in. + """ + manga: Manga! + + """ + The sequence number of this unit + """ + number: Int! + + """ + When this chapter was released + """ + releasedAt: ISO8601Date + + """ + A thumbnail image for the unit + """ + thumbnail: Image + + """ + The titles for this unit in various locales + """ + titles: TitlesList! + + updatedAt: ISO8601DateTime! + + """ + The volume this chapter is in. + """ + volume: Volume +} + +""" +The connection type for Chapter. +""" +type ChapterConnection { + """ + A list of edges. + """ + edges: [ChapterEdge] + + """ + A list of nodes. + """ + nodes: [Chapter] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ChapterEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Chapter +} + +enum ChapterSortEnum { + CREATED_AT + + UPDATED_AT + + NUMBER +} + +input ChapterSortOption { + on: ChapterSortEnum! + + direction: SortDirection! +} + +""" +Information about a Character in the Kitsu database +""" +type Character implements WithTimestamps { + createdAt: ISO8601DateTime! + + """ + A brief summary or description of the character. + """ + description(locales: [String!]): Map! + + id: ID! + + """ + An image of the character + """ + image: Image + + """ + Media this character appears in. + """ + media("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaCharacterConnection + + """ + The name for this character in various locales + """ + names: TitlesList + + """ + The original media this character showed up in + """ + primaryMedia: Media + + """ + The URL-friendly identifier of this character + """ + slug: String! + + updatedAt: ISO8601DateTime! +} + +enum CharacterRoleEnum { + """ + A character who appears throughout a series and is a focal point of the media + """ + MAIN + + """ + A character who appears in multiple episodes but is not a main character + """ + RECURRING + + """ + A background character who generally only appears in a few episodes + """ + BACKGROUND + + """ + A character from a different franchise making a (usually brief) appearance + """ + CAMEO +} + +""" +Information about a VA (Person) voicing a Character in a Media +""" +type CharacterVoice implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + The company who hired this voice actor to play this role + """ + licensor: Producer + + """ + The BCP47 locale tag for the voice acting role + """ + locale: String! + + """ + The MediaCharacter node + """ + mediaCharacter: MediaCharacter! + + """ + The person who voice acted this role + """ + person: Person! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for CharacterVoice. +""" +type CharacterVoiceConnection { + """ + A list of edges. + """ + edges: [CharacterVoiceEdge] + + """ + A list of nodes. + """ + nodes: [CharacterVoice] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CharacterVoiceEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CharacterVoice +} + +enum CharacterVoiceSortEnum { + CREATED_AT + + UPDATED_AT +} + +input CharacterVoiceSortOption { + on: CharacterVoiceSortEnum! + + direction: SortDirection! +} + +""" +A comment on a post +""" +type Comment implements Holdable & WithTimestamps { + """ + The user who created this comment for the parent post. + """ + author: Profile! + + """ + Unmodified content. + """ + content: String + + """ + Html formatted content. + """ + contentFormatted: String + + createdAt: ISO8601DateTime! + + """ + The reason this content was held for moderation + """ + heldReason: HeldReasonEnum + + id: ID! + + """ + Users who liked this comment + """ + likes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [CommentLikeSortOption]): ProfileConnection! + + """ + The parent comment if this comment was a reply to another. + """ + parent: Comment + + """ + The post that this comment is attached to. + """ + post: Post! + + """ + Replies to this comment + """ + replies("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [CommentSortOption]): CommentConnection! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Comment. +""" +type CommentConnection { + """ + A list of edges. + """ + edges: [CommentEdge] + + """ + A list of nodes. + """ + nodes: [Comment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +union CommentCreateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError|ValidationError + +input CommentCreateInput { + """ + The (markdown-formatted) content of the comment + """ + content: String! + + """ + The ID of the post to comment on + """ + postId: ID! + + """ + The ID of the parent comment when replying + """ + parentId: ID + + """ + The URL to embed in the comment + """ + embedUrl: String +} + +""" +Autogenerated return type of CommentCreate. +""" +type CommentCreatePayload { + errors: [CommentCreateErrorsUnion!] + + result: Comment +} + +union CommentDeleteErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input CommentDeleteInput { + """ + The id of the comment. + """ + commentId: ID! +} + +""" +Autogenerated return type of CommentDelete. +""" +type CommentDeletePayload { + errors: [CommentDeleteErrorsUnion!] + + result: Comment +} + +""" +An edge in a connection. +""" +type CommentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Comment +} + +enum CommentLikeSortEnum { + FOLLOWING + + CREATED_AT +} + +input CommentLikeSortOption { + on: CommentLikeSortEnum! + + direction: SortDirection! +} + +type CommentMutations { + """ + Create a Comment. + """ + create(input: CommentCreateInput!): CommentCreatePayload + + """ + Delete a Comment. + """ + delete(input: CommentDeleteInput!): CommentDeletePayload +} + +enum CommentSortEnum { + FOLLOWING + + CREATED_AT + + LIKES_COUNT +} + +input CommentSortOption { + on: CommentSortEnum! + + direction: SortDirection! +} + +""" +A date, expressed as an ISO8601 string +""" +scalar Date + +""" +An Episode of a Media +""" +type Episode implements Unit & WithTimestamps { + """ + The anime this episode is in + """ + anime: Anime! + + createdAt: ISO8601DateTime! + + """ + A brief summary or description of the unit + """ + description(locales: [String!]): Map! + + id: ID! + + """ + The length of the episode in seconds + """ + length: Int + + """ + The sequence number of this unit + """ + number: Int! + + """ + When this episode aired + """ + releasedAt: ISO8601DateTime + + """ + A thumbnail image for the unit + """ + thumbnail: Image + + """ + The titles for this unit in various locales + """ + titles: TitlesList! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Episode. +""" +type EpisodeConnection { + """ + A list of edges. + """ + edges: [EpisodeEdge] + + """ + A list of nodes. + """ + nodes: [Episode] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +input EpisodeCreateInput { + mediaId: ID! + + mediaType: MediaTypeEnum! + + titles: TitlesListInput! + + number: Int! + + description: Map + + length: Int + + releasedAt: Date + + thumbnailImage: Upload +} + +""" +Autogenerated return type of EpisodeCreate. +""" +type EpisodeCreatePayload { + episode: Episode + + errors: [Error!] +} + +""" +Autogenerated return type of EpisodeDelete. +""" +type EpisodeDeletePayload { + episode: GenericDelete + + errors: [Error!] +} + +""" +An edge in a connection. +""" +type EpisodeEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Episode +} + +type EpisodeMutations { + """ + Create an Episode. + """ + create("Create an Episode" input: EpisodeCreateInput!): EpisodeCreatePayload + + """ + Delete an Episode. + """ + delete("Delete an Episode" input: GenericDeleteInput!): EpisodeDeletePayload + + """ + Update an Episode. + """ + update("Update an Episode" input: EpisodeUpdateInput!): EpisodeUpdatePayload +} + +enum EpisodeSortEnum { + CREATED_AT + + UPDATED_AT + + NUMBER +} + +input EpisodeSortOption { + on: EpisodeSortEnum! + + direction: SortDirection! +} + +input EpisodeUpdateInput { + id: ID! + + titles: TitlesListInput + + number: Int + + description: Map + + length: Int + + releasedAt: Date + + thumbnailImage: Upload +} + +""" +Autogenerated return type of EpisodeUpdate. +""" +type EpisodeUpdatePayload { + episode: Episode + + errors: [Error!] +} + +""" +An episodic media in the Kitsu database +""" +interface Episodic { + """ + The number of episodes in this series + """ + episodeCount: Int + + """ + The general length (in seconds) of each episode + """ + episodeLength: Int + + """ + Episodes for this media + """ + episodes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, number: [Int!] @deprecated(reason: "This should only grab all episodes. Separate field will be provided for specific episodes"), sort: [EpisodeSortOption]): EpisodeConnection! + + """ + The total length (in seconds) of the entire series + """ + totalLength: Int +} + +""" +Generic error fields used by all errors. +""" +interface Error { + """ + The error code. + """ + code: String + + """ + A description of the error + """ + message: String! + + """ + Which input value this error came from + """ + path: [String!] +} + +enum ExternalIdentityProviderEnum { + """ + Facebook identity + """ + FACEBOOK +} + +""" +Favorite media, characters, and people for a user +""" +type Favorite implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + The kitsu object that is mapped + """ + item: FavoriteItemUnion! + + updatedAt: ISO8601DateTime! + + """ + The user who favorited this item + """ + user: Profile! +} + +""" +The connection type for Favorite. +""" +type FavoriteConnection { + """ + A list of edges. + """ + edges: [FavoriteEdge] + + """ + A list of nodes. + """ + nodes: [Favorite] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +union FavoriteCreateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input FavoriteCreateInput { + """ + The id of the entry + """ + id: ID! + + """ + The type of the entry. + """ + type: FavoriteEnum! +} + +""" +Autogenerated return type of FavoriteCreate. +""" +type FavoriteCreatePayload { + errors: [FavoriteCreateErrorsUnion!] + + result: Favorite +} + +union FavoriteDeleteErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input FavoriteDeleteInput { + """ + The id of the favorite entry. + """ + favoriteId: ID! +} + +""" +Autogenerated return type of FavoriteDelete. +""" +type FavoriteDeletePayload { + errors: [FavoriteDeleteErrorsUnion!] + + result: Favorite +} + +""" +An edge in a connection. +""" +type FavoriteEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Favorite +} + +enum FavoriteEnum { + ANIME + + MANGA + + CHARACTER + + PERSON +} + +""" +Objects which are Favoritable +""" +union FavoriteItemUnion = Anime|Character|Manga|Person + +type FavoriteMutations { + """ + Add a favorite entry + """ + create(input: FavoriteCreateInput!): FavoriteCreatePayload + + """ + Delete a favorite entry + """ + delete(input: FavoriteDeleteInput!): FavoriteDeletePayload +} + +""" +Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +""" +scalar Float + +enum FollowSortEnum { + FOLLOWING_FOLLOWED + + FOLLOWING_FOLLOWER + + CREATED_AT +} + +input FollowSortOption { + on: FollowSortEnum! + + direction: SortDirection! +} + +""" +Related media grouped together +""" +type Franchise implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + All media related to a franchise + """ + installments("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [InstallmentSortOption]): InstallmentConnection + + """ + The name of this franchise in various languages + """ + titles: TitlesList! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Franchise. +""" +type FranchiseConnection { + """ + A list of edges. + """ + edges: [FranchiseEdge] + + """ + A list of nodes. + """ + nodes: [Franchise] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type FranchiseEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Franchise +} + +type GenericDelete { + id: ID! +} + +input GenericDeleteInput { + id: ID! +} + +type GenericError implements Error { + """ + The error code. + """ + code: String + + """ + A description of the error + """ + message: String! + + """ + Which input value this error came from + """ + path: [String!] +} + +""" +The reason a record is held for manual moderator approval. +""" +enum HeldReasonEnum { + """ + Caught by a spam filter. + """ + SPAMFILTER + + """ + Caught by a word filter. + """ + WORDFILTER +} + +""" +User-generated content which can be held for moderation +""" +interface Holdable { + """ + The reason this content was held for moderation + """ + heldReason: HeldReasonEnum +} + +""" +Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) input value will be accepted as an ID. +""" +scalar ID + +""" +An ISO 8601-encoded date +""" +scalar ISO8601Date + +""" +An ISO 8601-encoded datetime +""" +scalar ISO8601DateTime + +type Image { + """ + A blurhash-encoded version of this image + """ + blurhash: String + + """ + The original image + """ + original: ImageView! + + """ + The various generated views of this image + """ + views(names: [String!]): [ImageView!]! +} + +type ImageView { + """ + The height of the image + """ + height: Int + + """ + The name of this view of the image + """ + name: String! + + """ + The URL of this view of the image + """ + url: String! + + """ + The width of the image + """ + width: Int +} + +""" +Individual media that belongs to a franchise +""" +type Installment implements WithTimestamps { + """ + Order based chronologically + """ + alternativeOrder: Int + + createdAt: ISO8601DateTime! + + """ + The franchise related to this installment + """ + franchise: Franchise! + + id: ID! + + """ + The media related to this installment + """ + media: Media! + + """ + Order based by date released + """ + releaseOrder: Int + + """ + Further explains the media relationship corresponding to a franchise + """ + tag: InstallmentTagEnum + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Installment. +""" +type InstallmentConnection { + """ + A list of edges. + """ + edges: [InstallmentEdge] + + """ + A list of nodes. + """ + nodes: [Installment] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type InstallmentEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Installment +} + +enum InstallmentSortEnum { + RELEASE_ORDER + + ALTERNATIVE_ORDER +} + +input InstallmentSortOption { + on: InstallmentSortEnum! + + direction: SortDirection! +} + +enum InstallmentTagEnum { + """ + The main story. + """ + MAIN_STORY + + """ + Takes place sometime during the main storyline. + """ + SIDE_STORY + + """ + Uses characters of a different series, but is not an alternate setting or story. + """ + SPINOFF + + """ + Characters from different media meet in the same story. + """ + CROSSOVER + + """ + Same universe/world/reality/timeline, completely different characters. + """ + ALTERNATE_SETTING + + """ + Same setting, same characters, story is told differently. + """ + ALTERNATE_VERSION +} + +""" +Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. +""" +scalar Int + +""" +Represents untyped JSON +""" +scalar JSON + +""" +The user library +""" +type Library { + """ + All Library Entries + """ + all("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum, status: [LibraryEntryStatusEnum!], sort: [LibraryEntrySortOption]): LibraryEntryConnection! + + """ + Library Entries with the completed status + """ + completed("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum, sort: [LibraryEntrySortOption]): LibraryEntryConnection! + + """ + Library Entries with the current status + """ + current("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum, sort: [LibraryEntrySortOption]): LibraryEntryConnection! + + """ + Library Entries with the dropped status + """ + dropped("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum, sort: [LibraryEntrySortOption]): LibraryEntryConnection! + + """ + Library Entries with the on_hold status + """ + onHold("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum, sort: [LibraryEntrySortOption]): LibraryEntryConnection! + + """ + Library Entries with the planned status + """ + planned("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum, sort: [LibraryEntrySortOption]): LibraryEntryConnection! + + """ + Random anime or manga from this library + """ + randomMedia(mediaType: MediaTypeEnum!, status: [LibraryEntryStatusEnum!]!): Media +} + +""" +Information about a specific media entry for a user +""" +type LibraryEntry implements WithTimestamps { + createdAt: ISO8601DateTime! + + """ + History of user actions for this library entry. + """ + events("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaTypes: [MediaTypeEnum!] = [ANIME]): LibraryEventConnection + + """ + When the user finished this media. + """ + finishedAt: ISO8601DateTime + + id: ID! + + """ + The last unit consumed + """ + lastUnit: Unit + + """ + The media related to this library entry. + """ + media: Media! + + """ + The next unit to be consumed + """ + nextUnit: Unit + + """ + Notes left by the profile related to this library entry. + """ + notes: String + + """ + If the media related to the library entry is Not-Safe-for-Work. + """ + nsfw: Boolean! + + """ + If this library entry is publicly visibile from their profile, or hidden. + """ + private: Boolean! + + """ + The number of episodes/chapters this user has watched/read + """ + progress: Int! + + """ + When the user last watched an episode or read a chapter of this media. + """ + progressedAt: ISO8601DateTime + + """ + How much you enjoyed this media (lower meaning not liking). + """ + rating: Int + + """ + The reaction based on the media of this library entry. + """ + reaction: MediaReaction + + """ + Amount of times this media has been rewatched. + """ + reconsumeCount: Int! + + """ + If the profile is currently rewatching this media. + """ + reconsuming: Boolean! + + """ + When the user started this media. + """ + startedAt: ISO8601DateTime + + """""" + status: LibraryEntryStatusEnum! + + updatedAt: ISO8601DateTime! + + """ + The user who created this library entry. + """ + user: Profile! + + """ + Volumes that the profile owns (physically or digital). + """ + volumesOwned: Int! +} + +""" +The connection type for LibraryEntry. +""" +type LibraryEntryConnection { + """ + A list of edges. + """ + edges: [LibraryEntryEdge] + + """ + A list of nodes. + """ + nodes: [LibraryEntry] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +input LibraryEntryCreateInput { + mediaId: ID! + + mediaType: MediaTypeEnum! + + status: LibraryEntryStatusEnum! + + progress: Int = 0 + + private: Boolean = false + + notes: String + + reconsumeCount: Int = 0 + + reconsuming: Boolean = false + + volumesOwned: Int = 0 + + rating: Int + + startedAt: ISO8601DateTime + + finishedAt: ISO8601DateTime +} + +""" +Autogenerated return type of LibraryEntryCreate. +""" +type LibraryEntryCreatePayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +""" +Autogenerated return type of LibraryEntryDelete. +""" +type LibraryEntryDeletePayload { + errors: [Error!] + + libraryEntry: GenericDelete +} + +""" +An edge in a connection. +""" +type LibraryEntryEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: LibraryEntry +} + +type LibraryEntryMutations { + """ + Create a library entry + """ + create("Create a Library Entry" input: LibraryEntryCreateInput!): LibraryEntryCreatePayload + + """ + Delete a library entry + """ + delete("Delete Library Entry" input: GenericDeleteInput!): LibraryEntryDeletePayload + + """ + Update a library entry + """ + update("Update Library Entry" input: LibraryEntryUpdateInput!): LibraryEntryUpdatePayload + + """ + Update library entry progress by id + """ + updateProgressById("Update library entry progress by id" input: LibraryEntryUpdateProgressByIdInput!): LibraryEntryUpdateProgressByIdPayload + + """ + Update library entry progress by media + """ + updateProgressByMedia("Update library entry progress by media" input: LibraryEntryUpdateProgressByMediaInput!): LibraryEntryUpdateProgressByMediaPayload + + """ + Update library entry rating by id + """ + updateRatingById("Update library entry rating by id" input: LibraryEntryUpdateRatingByIdInput!): LibraryEntryUpdateRatingByIdPayload + + """ + Update library entry rating by media + """ + updateRatingByMedia("Update library entry rating by media" input: LibraryEntryUpdateRatingByMediaInput!): LibraryEntryUpdateRatingByMediaPayload + + """ + Update library entry status by id + """ + updateStatusById("Update library entry status by id" input: LibraryEntryUpdateStatusByIdInput!): LibraryEntryUpdateStatusByIdPayload + + """ + Update library entry status by media + """ + updateStatusByMedia("Update library entry status by media" input: LibraryEntryUpdateStatusByMediaInput!): LibraryEntryUpdateStatusByMediaPayload +} + +enum LibraryEntrySortEnum { + MEDIA_TYPE + + STATUS + + CREATED_AT + + UPDATED_AT + + PROGRESS + + RATING + + STARTED_AT + + FINISHED_AT + + TITLE +} + +input LibraryEntrySortOption { + on: LibraryEntrySortEnum! + + direction: SortDirection! +} + +enum LibraryEntryStatusEnum { + """ + The user is currently reading or watching this media. + """ + CURRENT + + """ + The user plans to read or watch this media in future. + """ + PLANNED + + """ + The user completed this media. + """ + COMPLETED + + """ + The user started but paused reading or watching this media. + """ + ON_HOLD + + """ + The user started but chose not to finish this media. + """ + DROPPED +} + +input LibraryEntryUpdateInput { + id: ID! + + status: LibraryEntryStatusEnum + + progress: Int + + private: Boolean + + notes: String + + reconsumeCount: Int + + reconsuming: Boolean + + volumesOwned: Int + + rating: Int + + startedAt: ISO8601DateTime + + finishedAt: ISO8601DateTime +} + +""" +Autogenerated return type of LibraryEntryUpdate. +""" +type LibraryEntryUpdatePayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +input LibraryEntryUpdateProgressByIdInput { + id: ID! + + progress: Int! +} + +""" +Autogenerated return type of LibraryEntryUpdateProgressById. +""" +type LibraryEntryUpdateProgressByIdPayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +input LibraryEntryUpdateProgressByMediaInput { + mediaId: ID! + + mediaType: MediaTypeEnum! + + progress: Int! +} + +""" +Autogenerated return type of LibraryEntryUpdateProgressByMedia. +""" +type LibraryEntryUpdateProgressByMediaPayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +input LibraryEntryUpdateRatingByIdInput { + id: ID! + + """ + A number between 2 - 20 + """ + rating: Int! +} + +""" +Autogenerated return type of LibraryEntryUpdateRatingById. +""" +type LibraryEntryUpdateRatingByIdPayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +input LibraryEntryUpdateRatingByMediaInput { + mediaId: ID! + + mediaType: MediaTypeEnum! + + """ + A number between 2 - 20 + """ + rating: Int! +} + +""" +Autogenerated return type of LibraryEntryUpdateRatingByMedia. +""" +type LibraryEntryUpdateRatingByMediaPayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +input LibraryEntryUpdateStatusByIdInput { + id: ID! + + status: LibraryEntryStatusEnum! +} + +""" +Autogenerated return type of LibraryEntryUpdateStatusById. +""" +type LibraryEntryUpdateStatusByIdPayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +input LibraryEntryUpdateStatusByMediaInput { + mediaId: ID! + + mediaType: MediaTypeEnum! + + status: LibraryEntryStatusEnum! +} + +""" +Autogenerated return type of LibraryEntryUpdateStatusByMedia. +""" +type LibraryEntryUpdateStatusByMediaPayload { + errors: [Error!] + + libraryEntry: LibraryEntry +} + +""" +History of user actions for a library entry. +""" +type LibraryEvent implements WithTimestamps { + """ + The data that was changed for this library event. + """ + changedData: Map! + + createdAt: ISO8601DateTime! + + id: ID! + + """ + The type of library event. + """ + kind: LibraryEventKindEnum! + + """ + The library entry related to this library event. + """ + libraryEntry: LibraryEntry! + + """ + The media related to this library event. + """ + media: Media! + + updatedAt: ISO8601DateTime! + + """ + The user who created this library event + """ + user: Profile! +} + +""" +The connection type for LibraryEvent. +""" +type LibraryEventConnection { + """ + A list of edges. + """ + edges: [LibraryEventEdge] + + """ + A list of nodes. + """ + nodes: [LibraryEvent] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type LibraryEventEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: LibraryEvent +} + +enum LibraryEventKindEnum { + """ + Progress or Time Spent was added/updated. + """ + PROGRESSED + + """ + Status or Reconsuming was added/updated. + """ + UPDATED + + """ + Reaction was added/updated. + """ + REACTED + + """ + Rating was added/updated. + """ + RATED + + """ + Notes were added/updated. + """ + ANNOTATED +} + +enum LibraryEventSortEnum { + CREATED_AT + + UPDATED_AT +} + +input LibraryEventSortOption { + on: LibraryEventSortEnum! + + direction: SortDirection! +} + +enum LockedReasonEnum { + SPAM + + TOO_HEATED + + CLOSED +} + +type Manga implements Media & WithTimestamps { + """ + The recommended minimum age group for this media + """ + ageRating: AgeRatingEnum + + """ + An explanation of why this received the age rating it did + """ + ageRatingGuide: String + + """ + The average rating of this media amongst all Kitsu users + """ + averageRating: Float + + """ + The rank of this media by rating + """ + averageRatingRank: Int + + """ + A large banner image for this media + """ + bannerImage: Image + + """ + A list of categories for this media + """ + categories("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaCategorySortOption]): CategoryConnection! + + """ + Get a specific chapter of the manga. + """ + chapter(number: Int!): Chapter + + """ + The number of chapters in this manga. + """ + chapterCount: Int + + """ + The estimated number of chapters in this manga. + """ + chapterCountGuess: Int + + """ + The chapters in the manga. + """ + chapters("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [ChapterSortOption]): ChapterConnection + + """ + The characters who starred in this media + """ + characters("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaCharacterSortOption]): MediaCharacterConnection! + + createdAt: ISO8601DateTime! + + """ + A brief (mostly spoiler free) summary or description of the media. + """ + description(locales: [String!]): Map! + + """ + the day that this media made its final release + """ + endDate: Date + + """ + The number of users with this in their favorites + """ + favoritesCount: Int + + id: ID! + + """ + A list of mappings for this media + """ + mappings("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MappingConnection! + + """ + Your library entry related to this media. + """ + myLibraryEntry: LibraryEntry + + """ + A list of your wiki submissions for this media + """ + myWikiSubmissions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [WikiSubmissionSortOption]): WikiSubmissionConnection! + + """ + The time of the next release of this media + """ + nextRelease: ISO8601DateTime + + """ + The countries in which the media was originally primarily produced + """ + originCountries: [String!]! + + """ + The languages the media was originally produced in + """ + originLanguages: [String!]! + + """ + The country in which the media was primarily produced + """ + originalLocale: String @deprecated(reason: "Replaced with originCountries and originLanguages") + + """ + The poster image of this media + """ + posterImage: Image + + """ + All posts that tag this media. + """ + posts("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [PostSortOption]): PostConnection! + + """ + The companies which helped to produce this media + """ + productions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaProductionConnection! + + """ + A list of quotes from this media + """ + quotes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): QuoteConnection! + + """ + A list of reactions for this media + """ + reactions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaReactionSortOption]): MediaReactionConnection! + + """ + A list of relationships for this media + """ + relationships("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaRelationshipConnection! + + """ + Whether the media is Safe-for-Work + """ + sfw: Boolean! + + """ + The URL-friendly identifier of this media + """ + slug: String! + + """ + The staff members who worked on this media + """ + staff("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaStaffConnection! + + """ + The day that this media first released + """ + startDate: Date + + """ + The current releasing status of this media + """ + status: ReleaseStatusEnum! + + """ + A secondary type for categorizing Manga. + """ + subtype: MangaSubtypeEnum! + + """ + Description of when this media is expected to release + """ + tba: String + + """ + The titles for this media in various locales + """ + titles: TitlesList! + + """ + Anime or Manga. + """ + type: String! + + updatedAt: ISO8601DateTime! + + """ + The number of users with this in their library + """ + userCount: Int + + """ + The rank of this media by popularity + """ + userCountRank: Int + + """ + The number of volumes in this manga. + """ + volumeCount: Int + + """ + The volumes in the manga. + """ + volumes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [VolumeSortOption]): VolumeConnection +} + +type MangaAmountConsumed implements AmountConsumed { + """ + Total media completed atleast once. + """ + completed: Int! + + id: ID! + + """ + Total amount of media. + """ + media: Int! + + """ + The profile related to the user for this stat. + """ + profile: Profile! + + """ + Last time we fully recalculated this stat. + """ + recalculatedAt: ISO8601Date! + + """ + Total progress of library including reconsuming. + """ + units: Int! +} + +type MangaCategoryBreakdown implements CategoryBreakdown { + """ + A Map of category_id -> count for all categories present on the library entries + """ + categories: Map! + + id: ID! + + """ + The profile related to the user for this stat. + """ + profile: Profile! + + """ + Last time we fully recalculated this stat. + """ + recalculatedAt: ISO8601Date! + + """ + The total amount of library entries. + """ + total: Int! +} + +""" +The connection type for Manga. +""" +type MangaConnection { + """ + A list of edges. + """ + edges: [MangaEdge] + + """ + A list of nodes. + """ + nodes: [Manga] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MangaEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Manga +} + +enum MangaSubtypeEnum { + MANGA + + NOVEL + + """ + Chinese comics produced in China and in the Greater China region. + """ + MANHUA + + ONESHOT + + """ + Self published work. + """ + DOUJIN + + """ + A style of South Korean comic books and graphic novels + """ + MANHWA + + """ + Original English Language. + """ + OEL +} + +""" +A loose key-value map in GraphQL +""" +scalar Map + +""" +Media Mappings from External Sites (MAL, Anilist, etc..) to Kitsu. +""" +type Mapping implements WithTimestamps { + createdAt: ISO8601DateTime! + + """ + The ID of the media from the external site. + """ + externalId: ID! + + """ + The name of the site which kitsu media is being linked from. + """ + externalSite: MappingExternalSiteEnum! + + id: ID! + + """ + The kitsu object that is mapped. + """ + item: MappingItemUnion! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Mapping. +""" +type MappingConnection { + """ + A list of edges. + """ + edges: [MappingEdge] + + """ + A list of nodes. + """ + nodes: [Mapping] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +input MappingCreateInput { + externalSite: MappingExternalSiteEnum! + + externalId: ID! + + itemId: ID! + + itemType: MappingItemEnum! +} + +""" +Autogenerated return type of MappingCreate. +""" +type MappingCreatePayload { + errors: [Error!] + + mapping: Mapping +} + +""" +Autogenerated return type of MappingDelete. +""" +type MappingDeletePayload { + errors: [Error!] + + mapping: GenericDelete +} + +""" +An edge in a connection. +""" +type MappingEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Mapping +} + +enum MappingExternalSiteEnum { + MYANIMELIST_ANIME + + MYANIMELIST_MANGA + + MYANIMELIST_CHARACTERS + + MYANIMELIST_PEOPLE + + MYANIMELIST_PRODUCERS + + ANILIST_ANIME + + ANILIST_MANGA + + THETVDB + + THETVDB_SERIES + + THETVDB_SEASON + + ANIDB + + ANIMENEWSNETWORK + + MANGAUPDATES + + HULU + + IMDB_EPISODES + + AOZORA + + TRAKT + + MYDRAMALIST +} + +enum MappingItemEnum { + ANIME + + MANGA + + CATEGORY + + CHARACTER + + EPISODE + + PERSON + + PRODUCER +} + +""" +Objects which are Mappable +""" +union MappingItemUnion = Anime|Category|Character|Episode|Manga|Person|Producer + +type MappingMutations { + """ + Create a Mapping + """ + create("Create a Mapping" input: MappingCreateInput!): MappingCreatePayload + + """ + Delete a Mapping + """ + delete("Delete a Mapping" input: GenericDeleteInput!): MappingDeletePayload + + """ + Update a Mapping + """ + update("Update a Mapping" input: MappingUpdateInput!): MappingUpdatePayload +} + +input MappingUpdateInput { + id: ID! + + externalSite: MappingExternalSiteEnum + + externalId: ID + + itemId: ID + + itemType: MappingItemEnum +} + +""" +Autogenerated return type of MappingUpdate. +""" +type MappingUpdatePayload { + errors: [Error!] + + mapping: Mapping +} + +""" +A media in the Kitsu database +""" +interface Media { + """ + The recommended minimum age group for this media + """ + ageRating: AgeRatingEnum + + """ + An explanation of why this received the age rating it did + """ + ageRatingGuide: String + + """ + The average rating of this media amongst all Kitsu users + """ + averageRating: Float + + """ + The rank of this media by rating + """ + averageRatingRank: Int + + """ + A large banner image for this media + """ + bannerImage: Image + + """ + A list of categories for this media + """ + categories("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaCategorySortOption]): CategoryConnection! + + """ + The characters who starred in this media + """ + characters("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaCharacterSortOption]): MediaCharacterConnection! + + """ + A brief (mostly spoiler free) summary or description of the media. + """ + description(locales: [String!]): Map! + + """ + the day that this media made its final release + """ + endDate: Date + + """ + The number of users with this in their favorites + """ + favoritesCount: Int + + id: ID! + + """ + A list of mappings for this media + """ + mappings("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MappingConnection! + + """ + Your library entry related to this media. + """ + myLibraryEntry: LibraryEntry + + """ + A list of your wiki submissions for this media + """ + myWikiSubmissions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [WikiSubmissionSortOption]): WikiSubmissionConnection! + + """ + The time of the next release of this media + """ + nextRelease: ISO8601DateTime + + """ + The countries in which the media was originally primarily produced + """ + originCountries: [String!]! + + """ + The languages the media was originally produced in + """ + originLanguages: [String!]! + + """ + The country in which the media was primarily produced + """ + originalLocale: String @deprecated(reason: "Replaced with originCountries and originLanguages") + + """ + The poster image of this media + """ + posterImage: Image + + """ + All posts that tag this media. + """ + posts("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [PostSortOption]): PostConnection! + + """ + The companies which helped to produce this media + """ + productions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaProductionConnection! + + """ + A list of quotes from this media + """ + quotes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): QuoteConnection! + + """ + A list of reactions for this media + """ + reactions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaReactionSortOption]): MediaReactionConnection! + + """ + A list of relationships for this media + """ + relationships("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaRelationshipConnection! + + """ + Whether the media is Safe-for-Work + """ + sfw: Boolean! + + """ + The URL-friendly identifier of this media + """ + slug: String! + + """ + The staff members who worked on this media + """ + staff("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaStaffConnection! + + """ + The day that this media first released + """ + startDate: Date + + """ + The current releasing status of this media + """ + status: ReleaseStatusEnum! + + """ + Description of when this media is expected to release + """ + tba: String + + """ + The titles for this media in various locales + """ + titles: TitlesList! + + """ + Anime or Manga. + """ + type: String! + + """ + The number of users with this in their library + """ + userCount: Int + + """ + The rank of this media by popularity + """ + userCountRank: Int +} + +enum MediaCategorySortEnum { + ANCESTRY + + CREATED_AT +} + +input MediaCategorySortOption { + on: MediaCategorySortEnum! + + direction: SortDirection! +} + +""" +Information about a Character starring in a Media +""" +type MediaCharacter implements WithTimestamps { + """ + The character + """ + character: Character! + + createdAt: ISO8601DateTime! + + id: ID! + + """ + The media + """ + media: Media! + + """ + The role this character had in the media + """ + role: CharacterRoleEnum! + + updatedAt: ISO8601DateTime! + + """ + The voices of this character + """ + voices("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, locale: [String!], sort: [CharacterVoiceSortOption]): CharacterVoiceConnection +} + +""" +The connection type for MediaCharacter. +""" +type MediaCharacterConnection { + """ + A list of edges. + """ + edges: [MediaCharacterEdge] + + """ + A list of nodes. + """ + nodes: [MediaCharacter] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MediaCharacterEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MediaCharacter +} + +enum MediaCharacterSortEnum { + CREATED_AT + + UPDATED_AT + + ROLE +} + +input MediaCharacterSortOption { + on: MediaCharacterSortEnum! + + direction: SortDirection! +} + +""" +The connection type for Media. +""" +type MediaConnection { + """ + A list of edges. + """ + edges: [MediaEdge] + + """ + A list of nodes. + """ + nodes: [Media] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type MediaEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Media +} + +""" +The role a company played in the creation or localization of a media +""" +type MediaProduction implements WithTimestamps { + """ + The production company + """ + company: Producer! + + createdAt: ISO8601DateTime! + + id: ID! + + """ + The media + """ + media: Media! + + """ + The role this company played + """ + role: MediaProductionRoleEnum! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for MediaProduction. +""" +type MediaProductionConnection { + """ + A list of edges. + """ + edges: [MediaProductionEdge] + + """ + A list of nodes. + """ + nodes: [MediaProduction] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MediaProductionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MediaProduction +} + +enum MediaProductionRoleEnum { + PRODUCER + + LICENSOR + + STUDIO + + SERIALIZATION +} + +""" +A simple review that is 140 characters long expressing how you felt about a media +""" +type MediaReaction implements Holdable & WithTimestamps { + """ + The author who wrote this reaction. + """ + author: Profile! + + createdAt: ISO8601DateTime! + + """ + Whether you have liked this media reaction + """ + hasLiked: Boolean! + + """ + The reason this content was held for moderation + """ + heldReason: HeldReasonEnum + + id: ID! + + """ + The library entry related to this reaction. + """ + libraryEntry: LibraryEntry! + + """ + Users that have liked this reaction + """ + likes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaReactionVoteSortOption]): ProfileConnection! + + """ + The media related to this reaction. + """ + media: Media! + + """ + When this media reaction was written based on media progress. + """ + progress: Int! + + """ + The reaction text related to a media. + """ + reaction: String! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for MediaReaction. +""" +type MediaReactionConnection { + """ + A list of edges. + """ + edges: [MediaReactionEdge] + + """ + A list of nodes. + """ + nodes: [MediaReaction] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +union MediaReactionCreateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError|ValidationError + +input MediaReactionCreateInput { + """ + The ID of the entry in your library to react to + """ + libraryEntryId: ID! + + """ + The text of the reaction to the media + """ + reaction: String! +} + +""" +Autogenerated return type of MediaReactionCreate. +""" +type MediaReactionCreatePayload { + errors: [MediaReactionCreateErrorsUnion!] + + result: MediaReaction +} + +union MediaReactionDeleteErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input MediaReactionDeleteInput { + """ + The reaction to delete + """ + mediaReactionId: ID! +} + +""" +Autogenerated return type of MediaReactionDelete. +""" +type MediaReactionDeletePayload { + errors: [MediaReactionDeleteErrorsUnion!] + + result: MediaReaction +} + +""" +An edge in a connection. +""" +type MediaReactionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MediaReaction +} + +union MediaReactionLikeErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input MediaReactionLikeInput { + """ + The reaction to like + """ + mediaReactionId: ID! +} + +""" +Autogenerated return type of MediaReactionLike. +""" +type MediaReactionLikePayload { + errors: [MediaReactionLikeErrorsUnion!] + + result: MediaReaction +} + +type MediaReactionMutations { + """ + Share a brief reaction to a media, tied to your library entry + """ + create(input: MediaReactionCreateInput!): MediaReactionCreatePayload + + """ + Delete a mutation + """ + delete(input: MediaReactionDeleteInput!): MediaReactionDeletePayload + + """ + Like a media reaction + """ + like(input: MediaReactionLikeInput!): MediaReactionLikePayload + + """ + Remove your like from a media reaction + """ + unlike(input: MediaReactionUnlikeInput!): MediaReactionUnlikePayload +} + +enum MediaReactionSortEnum { + CREATED_AT + + UPDATED_AT + + UP_VOTES_COUNT +} + +input MediaReactionSortOption { + on: MediaReactionSortEnum! + + direction: SortDirection! +} + +union MediaReactionUnlikeErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input MediaReactionUnlikeInput { + """ + The reaction to remove your like from + """ + mediaReactionId: ID! +} + +""" +Autogenerated return type of MediaReactionUnlike. +""" +type MediaReactionUnlikePayload { + errors: [MediaReactionUnlikeErrorsUnion!] + + result: MediaReaction +} + +enum MediaReactionVoteSortEnum { + CREATED_AT + + FOLLOWING +} + +input MediaReactionVoteSortOption { + on: MediaReactionVoteSortEnum! + + direction: SortDirection! +} + +""" +A relationship from one media to another +""" +type MediaRelationship implements WithTimestamps { + createdAt: ISO8601DateTime! + + """ + The destination media + """ + destination: Media! + + """ + The kind of relationship + """ + kind: MediaRelationshipKindEnum! + + """ + The source media + """ + source: Media! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for MediaRelationship. +""" +type MediaRelationshipConnection { + """ + A list of edges. + """ + edges: [MediaRelationshipEdge] + + """ + A list of nodes. + """ + nodes: [MediaRelationship] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MediaRelationshipEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MediaRelationship +} + +""" +The relationship kind from one media entry to another +""" +enum MediaRelationshipKindEnum { + SEQUEL + + PREQUEL + + ALTERNATIVE_SETTING + + ALTERNATIVE_VERSION + + SIDE_STORY + + PARENT_STORY + + SUMMARY + + FULL_STORY + + SPINOFF + + ADAPTATION + + CHARACTER + + OTHER +} + +""" +Information about a person working on an anime +""" +type MediaStaff implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + The media + """ + media: Media! + + """ + The person + """ + person: Person! + + """ + The role this person had in the creation of this media + """ + role: String! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for MediaStaff. +""" +type MediaStaffConnection { + """ + A list of edges. + """ + edges: [MediaStaffEdge] + + """ + A list of nodes. + """ + nodes: [MediaStaff] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type MediaStaffEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: MediaStaff +} + +""" +これはアニメやマンガです +""" +enum MediaTypeEnum { + ANIME + + MANGA +} + +type Mutation { + account: AccountMutations! + + """ + Create a new Kitsu account + """ + accountCreate(input: AccountCreateInput!): AccountCreatePayload + + anime: AnimeMutations! + + block: BlockMutations! + + comment: CommentMutations! + + episode: EpisodeMutations! + + favorite: FavoriteMutations! + + libraryEntry: LibraryEntryMutations! + + mapping: MappingMutations! + + mediaReaction: MediaReactionMutations! + + post: PostMutations! + + pro: ProMutations! + + profile: ProfileMutations! + + profileLink: ProfileLinkMutations! + + wikiSubmission: WikiSubmissionMutations! +} + +""" +The mutation requires an authenticated logged-in user session, and none was provided or the session has expired. The recommended action varies depending on your application and whether you provided the bearer token in the `Authorization` header or not. If you did, you should probably attempt to refresh the token, and if that fails, prompt the user to log in again. If you did not provide a bearer token, you should just prompt the user to log in. +""" +type NotAuthenticatedError implements Error { + """ + The error code. + """ + code: String + + """ + A description of the error + """ + message: String! + + """ + Which input value this error came from + """ + path: [String!] +} + +""" +The mutation requires higher permissions than the current user or token has. This is a bit vague, but it generally means you're attempting to modify an object you don't own, or perform an administrator action without being an administrator. It could also mean your token does not have the required scopes to perform the action. The recommended action is to display a message to the user informing them that their action failed and that retrying will generally *not* help. +""" +type NotAuthorizedError implements Error { + action: String + + """ + The error code. + """ + code: String + + """ + A description of the error + """ + message: String! + + """ + Which input value this error came from + """ + path: [String!] +} + +""" +An object required for your mutation was unable to be located. Usually this means the object you're attempting to modify or delete does not exist. The recommended action is to display a message to the user informing them that their action failed and that retrying will generally *not* help. +""" +type NotFoundError implements Error { + """ + The error code. + """ + code: String + + """ + A description of the error + """ + message: String! + + """ + Which input value this error came from + """ + path: [String!] +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String + + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String +} + +""" +A Voice Actor, Director, Animator, or other person who works in the creation and localization of media +""" +type Person implements WithTimestamps { + """ + The day when this person was born + """ + birthday: Date + + createdAt: ISO8601DateTime! + + """ + A brief biography or description of the person. + """ + description(locales: [String!]): Map! + + id: ID! + + """ + An image of the person + """ + image: Image + + """ + Information about the person working on specific media + """ + mediaStaff("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MediaStaffConnection + + """ + The primary name of this person. + """ + name: String! + + """ + The name of this person in various languages + """ + names: TitlesList! + + """ + The URL-friendly identifier of this person. + """ + slug: String! + + updatedAt: ISO8601DateTime! + + """ + The voice-acting roles this person has had. + """ + voices("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): CharacterVoiceConnection +} + +""" +A post that is visible to your followers and globally in the news-feed. +""" +type Post implements Holdable & WithTimestamps { + """ + The user who created this post. + """ + author: Profile! + + """ + All comments on this post + """ + comments("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [CommentSortOption]): CommentConnection! + + """ + Unmodified content. + """ + content: String + + """ + Html formatted content. + """ + contentFormatted: String + + createdAt: ISO8601DateTime! + + """ + Users that are watching this post + """ + follows("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): ProfileConnection! + + """ + The reason this content was held for moderation + """ + heldReason: HeldReasonEnum + + id: ID! + + """ + If a post is Not-Safe-for-Work. + """ + isNsfw: Boolean! + + """ + If this post spoils the tagged media. + """ + isSpoiler: Boolean! + + """ + Users that have liked this post + """ + likes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [PostLikeSortOption]): ProfileConnection! + + """ + When this post was locked. + """ + lockedAt: ISO8601DateTime + + """ + The user who locked this post. + """ + lockedBy: Profile + + """ + The reason why this post was locked. + """ + lockedReason: LockedReasonEnum + + """ + The media tagged in this post. + """ + media: Media + + """ + The profile of the target user of the post. + """ + targetProfile: Profile! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Post. +""" +type PostConnection { + """ + A list of edges. + """ + edges: [PostEdge] + + """ + A list of nodes. + """ + nodes: [Post] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +input PostCreateInput { + content: String! + + mediaId: ID + + mediaType: MediaTypeEnum + + isSpoiler: Boolean = false + + isNsfw: Boolean = false + + spoiledUnitId: ID + + spoiledUnitType: String + + targetUserId: ID +} + +""" +Autogenerated return type of PostCreate. +""" +type PostCreatePayload { + errors: [Error!] + + post: Post +} + +union PostDeleteErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input PostDeleteInput { + """ + The id of the post. + """ + postId: ID! +} + +""" +Autogenerated return type of PostDelete. +""" +type PostDeletePayload { + errors: [PostDeleteErrorsUnion!] + + result: Post +} + +""" +An edge in a connection. +""" +type PostEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Post +} + +enum PostLikeSortEnum { + FOLLOWING + + CREATED_AT +} + +input PostLikeSortOption { + on: PostLikeSortEnum! + + direction: SortDirection! +} + +input PostLockInput { + id: ID! + + lockedReason: LockedReasonEnum! +} + +""" +Autogenerated return type of PostLock. +""" +type PostLockPayload { + errors: [Error!] + + post: Post +} + +type PostMutations { + """ + Create a Post. + """ + create("Create a Post" input: PostCreateInput!): PostCreatePayload + + """ + Delete a Post. + """ + delete(input: PostDeleteInput!): PostDeletePayload + + """ + Lock a Post. + """ + lock("Lock a Post." input: PostLockInput!): PostLockPayload + + """ + Unlock a Post. + """ + unlock("Unlock a Post." input: PostUnlockInput!): PostUnlockPayload +} + +enum PostSortEnum { + CREATED_AT +} + +input PostSortOption { + on: PostSortEnum! + + direction: SortDirection! +} + +input PostUnlockInput { + id: ID! +} + +""" +Autogenerated return type of PostUnlock. +""" +type PostUnlockPayload { + errors: [Error!] + + post: Post +} + +type ProMutations { + """ + Set the user's discord tag + """ + setDiscord("Your discord tag (Name#1234)" discord: String!): ProSetDiscordPayload + + """ + Set the user's Hall-of-Fame message + """ + setMessage("The message to set for your Hall of Fame entry" message: String!): ProSetMessagePayload + + """ + End the user's pro subscription + """ + unsubscribe: ProUnsubscribePayload +} + +""" +Autogenerated return type of ProSetDiscord. +""" +type ProSetDiscordPayload { + discord: String! +} + +""" +Autogenerated return type of ProSetMessage. +""" +type ProSetMessagePayload { + message: String! +} + +""" +A subscription to Kitsu PRO +""" +type ProSubscription implements WithTimestamps { + """ + The account which is subscribed to Pro benefits + """ + account: Account! + + """ + The billing service used for this subscription + """ + billingService: RecurringBillingServiceEnum! + + createdAt: ISO8601DateTime! + + """ + The tier of Pro the account is subscribed to + """ + tier: ProTierEnum! + + updatedAt: ISO8601DateTime! +} + +enum ProTierEnum { + """ + Aozora Pro (only hides ads) + """ + AO_PRO @deprecated(reason: "No longer for sale") + + """ + Aozora Pro+ (only hides ads) + """ + AO_PRO_PLUS @deprecated(reason: "No longer for sale") + + """ + Basic tier of Kitsu Pro + """ + PRO + + """ + Top tier of Kitsu Pro + """ + PATRON +} + +""" +Autogenerated return type of ProUnsubscribe. +""" +type ProUnsubscribePayload { + expiresAt: ISO8601DateTime +} + +""" +A company involved in the creation or localization of media +""" +type Producer implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + The name of this production company + """ + name: String! + + updatedAt: ISO8601DateTime! +} + +""" +A user profile on Kitsu +""" +type Profile implements WithTimestamps { + """ + A short biographical blurb about this profile + """ + about: String + + """ + An avatar image to easily identify this profile + """ + avatarImage: Image + + """ + A banner to display at the top of the profile + """ + bannerImage: Image + + """ + When the user was born + """ + birthday: ISO8601Date + + """ + All comments to any post this user has made. + """ + comments("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): CommentConnection! + + createdAt: ISO8601DateTime! + + """ + Favorite media, characters, and people + """ + favorites("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): FavoriteConnection! + + """ + People that follow the user + """ + followers("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [FollowSortOption]): ProfileConnection! + + """ + People the user is following + """ + following("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [FollowSortOption]): ProfileConnection! + + """ + What the user identifies as + """ + gender: String + + id: ID! + + """ + The user library of their media + """ + library: Library! + + """ + A list of library events for this user + """ + libraryEvents("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [LibraryEventSortOption], "Will return all if not supplied" kind: [LibraryEventKindEnum!] = [PROGRESSED,UPDATED,REACTED,RATED,ANNOTATED]): LibraryEventConnection! + + """ + The user's general location + """ + location: String + + """ + Media reactions written by this user. + """ + mediaReactions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [MediaReactionSortOption]): MediaReactionConnection! + + """ + A non-unique publicly visible name for the profile. Minimum of 3 characters and any valid Unicode character + """ + name: String! + + """ + Post pinned to the user profile + """ + pinnedPost: Post + + """ + All posts this profile has made. + """ + posts("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [PostSortOption]): PostConnection! + + """ + The message this user has submitted to the Hall of Fame + """ + proMessage: String + + """ + The PRO level the user currently has + """ + proTier: ProTierEnum + + """ + Reviews created by this user + """ + reviews("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [WikiSubmissionSortOption]): ReviewConnection + + """ + Links to the user on other (social media) sites. + """ + siteLinks("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): SiteLinkConnection + + """ + The URL-friendly identifier for this profile + """ + slug: String + + """ + The different stats we calculate for this user. + """ + stats: ProfileStats! + + updatedAt: ISO8601DateTime! + + """ + A fully qualified URL to the profile + """ + url: String + + """ + The character this profile has declared as their waifu or husbando + """ + waifu: Character + + """ + The properly-gendered term for the user's waifu. This should normally only be 'Waifu' or 'Husbando' but some people are jerks, including the person who wrote this... + """ + waifuOrHusbando: String + + """ + Wiki submissions created by this user + """ + wikiSubmissions("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [WikiSubmissionSortOption], "Will return all if not supplied" statuses: [WikiSubmissionStatusEnum!] = [DRAFT,PENDING,APPROVED,REJECTED]): WikiSubmissionConnection! +} + +""" +The connection type for Profile. +""" +type ProfileConnection { + """ + A list of edges. + """ + edges: [ProfileEdge] + + """ + A list of nodes. + """ + nodes: [Profile] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProfileEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Profile +} + +union ProfileLinkCreateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError|ValidationError + +input ProfileLinkCreateInput { + """ + The url of the profile link + """ + url: String! + + """ + The website. + """ + profileLinkSite: ProfileLinksSitesEnum! +} + +""" +Autogenerated return type of ProfileLinkCreate. +""" +type ProfileLinkCreatePayload { + errors: [ProfileLinkCreateErrorsUnion!] + + result: SiteLink +} + +union ProfileLinkDeleteErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input ProfileLinkDeleteInput { + """ + The profile link to delete + """ + profileLink: ProfileLinksSitesEnum! +} + +""" +Autogenerated return type of ProfileLinkDelete. +""" +type ProfileLinkDeletePayload { + errors: [ProfileLinkDeleteErrorsUnion!] + + result: SiteLink +} + +type ProfileLinkMutations { + """ + Add a profile link. + """ + create(input: ProfileLinkCreateInput!): ProfileLinkCreatePayload + + """ + Delete a profile link. + """ + delete(input: ProfileLinkDeleteInput!): ProfileLinkDeletePayload + + """ + Update a profile link. + """ + update(input: ProfileLinkUpdateInput!): ProfileLinkUpdatePayload +} + +""" +An external site that can be linked to a user. +""" +type ProfileLinkSite implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + Name of the external profile website. + """ + name: String! + + updatedAt: ISO8601DateTime! + + """ + Regex pattern used to validate the profile link. + """ + validateFind: String! + + """ + Pattern to be replaced after validation. + """ + validateReplace: String! +} + +union ProfileLinkUpdateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError|ValidationError + +input ProfileLinkUpdateInput { + """ + The url of the profile link + """ + url: String! + + """ + The website. + """ + profileLinkSite: ProfileLinksSitesEnum! +} + +""" +Autogenerated return type of ProfileLinkUpdate. +""" +type ProfileLinkUpdatePayload { + errors: [ProfileLinkUpdateErrorsUnion!] + + result: SiteLink +} + +enum ProfileLinksSitesEnum { + TWITTER + + FACEBOOK + + YOUTUBE + + GOOGLE + + INSTAGRAM + + TWITCH + + VIMEO + + GITHUB + + BATTLENET + + STEAM + + RAPTR + + DISCORD + + TUMBLR + + SOUNDCLOUD + + DAILYMOTION + + KICKSTARTER + + MOBCRUSH + + OSU + + PATREON + + DEVIANTART + + DRIBBBLE + + IMDB + + LASTFM + + LETTERBOXD + + MEDIUM + + PLAYERME + + REDDIT + + TRAKT + + WEBSITE +} + +type ProfileMutations { + """ + Update the profile of the current user. + """ + update(input: ProfileUpdateInput!): ProfileUpdatePayload +} + +""" +The different types of user stats that we calculate. +""" +type ProfileStats { + """ + The total amount of anime you have watched over your whole life. + """ + animeAmountConsumed: AnimeAmountConsumed! + + """ + The breakdown of the different categories related to the anime you have completed + """ + animeCategoryBreakdown: AnimeCategoryBreakdown! + + """ + The total amount of manga you ahve read over your whole life. + """ + mangaAmountConsumed: MangaAmountConsumed! + + """ + The breakdown of the different categories related to the manga you have completed + """ + mangaCategoryBreakdown: MangaCategoryBreakdown! +} + +union ProfileUpdateErrorsUnion = NotAuthenticatedError|NotAuthorizedError|NotFoundError + +input ProfileUpdateInput { + """ + Your ID or the one of another user. + """ + id: ID + + """ + The display name of the user + """ + name: String + + """ + The slug (@username) of the user + """ + slug: String + + """ + About section of the profile. + """ + about: String + + """ + The user preference of their partner. + """ + waifuOrHusbando: WaifuOrHusbandoEnum + + """ + The id of the waifu or husbando. + """ + waifuId: ID + + """ + The preferred gender of the user. + """ + gender: String + + """ + The birthday of the user. + """ + birthday: Date +} + +""" +Autogenerated return type of ProfileUpdate. +""" +type ProfileUpdatePayload { + errors: [ProfileUpdateErrorsUnion!] + + result: Profile +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + + _service: _Service! + + """ + All Anime in the Kitsu database + """ + anime("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): AnimeConnection! + + """ + All Anime with specific Status + """ + animeByStatus("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, status: ReleaseStatusEnum!): AnimeConnection + + """ + All blocked user of the current account. + """ + blocks("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): BlockConnection + + """ + All Categories in the Kitsu Database + """ + categories("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): CategoryConnection + + """ + Kitsu account details. You must supply an Authorization token in header. + """ + currentAccount: Account + + """ + Your Kitsu profile. You must supply an Authorization token in header. + """ + currentProfile: Profile + + """ + Find a single Anime by ID + """ + findAnimeById(id: ID!): Anime + + """ + Find a single Anime by Slug + """ + findAnimeBySlug(slug: String!): Anime + + """ + Find a single Category by ID + """ + findCategoryById(id: ID!): Category + + """ + Find a single Category by Slug + """ + findCategoryBySlug(slug: String!): Category + + """ + Find a single Manga Chapter by ID + """ + findChapterById(id: ID!): Chapter + + """ + Find a single Character by ID + """ + findCharacterById(id: ID!): Character + + """ + Find a single Character by Slug + """ + findCharacterBySlug(slug: String!): Character + + """ + Find a single Library Entry by ID + """ + findLibraryEntryById(id: ID!): LibraryEntry + + """ + Find a single Library Event by ID + """ + findLibraryEventById(id: ID!): LibraryEvent + + """ + Find a single Manga by ID + """ + findMangaById(id: ID!): Manga + + """ + Find a single Manga by Slug + """ + findMangaBySlug(slug: String!): Manga + + """ + Find a single Media by ID and Type + """ + findMediaByIdAndType(id: ID!, mediaType: MediaTypeEnum!): Media + + """ + Find a single Person by ID + """ + findPersonById(id: ID!): Person + + """ + Find a single Person by Slug + """ + findPersonBySlug(slug: String!): Person + + """ + Find a single Post by ID + """ + findPostById(id: ID!): Post + + """ + Find a single User by ID + """ + findProfileById(id: ID!): Profile + + """ + Find a single User by Slug + """ + findProfileBySlug(slug: String!): Profile + + """ + Find a single Report by ID + """ + findReportById(id: ID!): Report + + """ + Find a single Wiki Submission by ID + """ + findWikiSubmissionById(id: ID!): WikiSubmission + + """ + All Franchise in the Kitsu database + """ + franchises("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): FranchiseConnection + + """ + List trending media on Kitsu + """ + globalTrending("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum!): MediaConnection! + + """ + List of Library Entries by MediaType and MediaId + """ + libraryEntriesByMedia("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum!, mediaId: ID!): LibraryEntryConnection + + """ + List of Library Entries by MediaType + """ + libraryEntriesByMediaType("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum!): LibraryEntryConnection + + """ + List trending media within your network + """ + localTrending("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, mediaType: MediaTypeEnum!): MediaConnection! + + """ + Find a specific Mapping Item by External ID and External Site. + """ + lookupMapping(externalId: ID!, externalSite: MappingExternalSiteEnum!): MappingItemUnion + + """ + All Manga in the Kitsu database + """ + manga("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): MangaConnection! + + """ + All Manga with specific Status + """ + mangaByStatus("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, status: ReleaseStatusEnum!): MangaConnection + + """ + Patrons sorted by a Proprietary Magic Algorithm + """ + patrons("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): ProfileConnection! + + """ + Random anime or manga + """ + randomMedia(mediaType: MediaTypeEnum!, ageRatings: [AgeRatingEnum!]!): Media! + + """ + All Reports in the Kitsu database + """ + reports("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): ReportConnection + + """ + Select all Reports that match with a supplied status. + """ + reportsByStatus("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, "Will return all if not supplied" statuses: [ReportStatusEnum!] = [REPORTED,RESOLVED,DECLINED]): ReportConnection + + """ + Search for Anime by title using Algolia. The most relevant results will be at the top. + """ + searchAnimeByTitle("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, title: String!): AnimeConnection! + + """ + Search for Manga by title using Algolia. The most relevant results will be at the top. + """ + searchMangaByTitle("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, title: String!): MangaConnection! + + """ + Search for any media (Anime, Manga) by title using Algolia. If no media_type is supplied, it will search for both. The most relevant results will be at the top. + """ + searchMediaByTitle("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, title: String!, "Dynamically choose a specific media_type. If left blank, it will return results for both." mediaType: MediaTypeEnum): MediaConnection! + + """ + Search for User by username using Algolia. The most relevant results will be at the top. + """ + searchProfileByUsername("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, username: String!): ProfileConnection + + """ + Get your current session info + """ + session: Session! + + """ + Select all Wiki Submissions that match with a supplied status. + """ + wikiSubmissionsByStatuses("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int, sort: [WikiSubmissionSortOption], "Will return all if not supplied" statuses: [WikiSubmissionStatusEnum!] = [DRAFT,PENDING,APPROVED,REJECTED]): WikiSubmissionConnection +} + +""" +A quote from a media +""" +type Quote implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + The lines of the quote + """ + lines("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): QuoteLineConnection! + + """ + The media this quote is excerpted from + """ + media: Media! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Quote. +""" +type QuoteConnection { + """ + A list of edges. + """ + edges: [QuoteEdge] + + """ + A list of nodes. + """ + nodes: [Quote] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type QuoteEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Quote +} + +""" +A line in a quote +""" +type QuoteLine implements WithTimestamps { + """ + The character who said this line + """ + character: Character! + + """ + The line that was spoken + """ + content: String! + + createdAt: ISO8601DateTime! + + id: ID! + + """ + The quote this line is in + """ + quote: Quote! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for QuoteLine. +""" +type QuoteLineConnection { + """ + A list of edges. + """ + edges: [QuoteLineEdge] + + """ + A list of nodes. + """ + nodes: [QuoteLine] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type QuoteLineEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: QuoteLine +} + +enum RatingSystemEnum { + """ + 1-20 displayed as 4 smileys - Awful (1), Meh (8), Good (14) and Great (20) + """ + SIMPLE + + """ + 1-20 in increments of 2 displayed as 5 stars in 0.5 star increments + """ + REGULAR + + """ + 1-20 in increments of 1 displayed as 1-10 in 0.5 increments + """ + ADVANCED +} + +enum RecurringBillingServiceEnum { + """ + Bill a credit card via Stripe + """ + STRIPE + + """ + Bill a PayPal account + """ + PAYPAL + + """ + Billed through Apple In-App Subscription + """ + APPLE + + """ + Billed through Google Play Subscription + """ + GOOGLE_PLAY +} + +enum ReleaseSeasonEnum { + """ + Released during the Winter season + """ + WINTER + + """ + Released during the Spring season + """ + SPRING + + """ + Released during the Summer season + """ + SUMMER + + """ + Released during the Fall season + """ + FALL +} + +enum ReleaseStatusEnum { + """ + The release date has not been announced yet + """ + TBA + + """ + This media is no longer releasing + """ + FINISHED + + """ + This media is currently releasing + """ + CURRENT + + """ + This media is releasing soon + """ + UPCOMING + + """ + This media is not released yet + """ + UNRELEASED +} + +""" +A report made by a user +""" +type Report implements WithTimestamps { + createdAt: ISO8601DateTime! + + """ + Additional information related to why the report was made + """ + explanation: String + + id: ID! + + """ + The moderator who responded to this report + """ + moderator: Profile + + """ + The entity that the report is related to + """ + naughty: ReportItemUnion + + """ + The reason for why the report was made + """ + reason: ReportReasonEnum! + + """ + The user who made this report + """ + reporter: Profile! + + """ + The resolution status for this report + """ + status: ReportStatusEnum! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Report. +""" +type ReportConnection { + """ + A list of edges. + """ + edges: [ReportEdge] + + """ + A list of nodes. + """ + nodes: [Report] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReportEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Report +} + +""" +Objects which are Reportable +""" +union ReportItemUnion = Comment|MediaReaction|Post|Review + +enum ReportReasonEnum { + """ + Not Safe For Work + """ + NSFW + + """""" + OFFENSIVE + + """""" + SPOILER + + """ + No bulli! + """ + BULLYING + + """""" + SPAM + + """""" + OTHER +} + +enum ReportStatusEnum { + """""" + REPORTED + + """""" + RESOLVED + + """""" + DECLINED +} + +""" +A media review made by a user +""" +type Review implements WithTimestamps { + """ + The author who wrote this review. + """ + author: Profile! + + """ + The review data + """ + content: String! + + createdAt: ISO8601DateTime! + + """ + The review data formatted + """ + formattedContent: String! + + id: ID! + + """ + Does this review contain spoilers from the media + """ + isSpoiler: Boolean! + + """ + The library entry related to this review. + """ + libraryEntry: LibraryEntry! + + """ + Users who liked this review + """ + likes("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): ProfileConnection! + + """ + The media related to this review. + """ + media: Media! + + """ + When this review was written based on media progress. + """ + progress: Int! + + """ + The user rating for this media + """ + rating: Int! + + """ + Potentially migrated over from hummingbird. + """ + source: String! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Review. +""" +type ReviewConnection { + """ + A list of edges. + """ + edges: [ReviewEdge] + + """ + A list of nodes. + """ + nodes: [Review] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ReviewEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Review +} + +""" +Information about a user session +""" +type Session { + """ + The account associated with this session + """ + account: Account + + """ + Single sign-on token for Nolt + """ + noltToken: String! + + """ + The profile associated with this session + """ + profile: Profile +} + +enum SfwFilterPreferenceEnum { + """ + Hide All Adult Content + """ + SFW + + """ + Limit to Following Feed + """ + NSFW_SOMETIMES + + """ + Adult Content Everywhere + """ + NSFW_EVERYWHERE +} + +""" +A link to a user's profile on an external site. +""" +type SiteLink implements WithTimestamps { + """ + The user profile the site is linked to. + """ + author: Profile! + + createdAt: ISO8601DateTime! + + id: ID! + + """ + The actual linked website. + """ + site: ProfileLinkSite! + + updatedAt: ISO8601DateTime! + + """ + A fully qualified URL of the user profile on an external site. + """ + url: String! +} + +""" +The connection type for SiteLink. +""" +type SiteLinkConnection { + """ + A list of edges. + """ + edges: [SiteLinkEdge] + + """ + A list of nodes. + """ + nodes: [SiteLink] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type SiteLinkEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: SiteLink +} + +enum SitePermissionEnum { + """ + Administrator/staff member of Kitsu + """ + ADMIN + + """ + Moderator of community behavior + """ + COMMUNITY_MOD + + """ + Maintainer of the Kitsu media database + """ + DATABASE_MOD +} + +enum SiteThemeEnum { + """ + Light Theme + """ + LIGHT + + """ + Dark Theme + """ + DARK +} + +enum SortDirection { + ASCENDING + + DESCENDING +} + +""" +Media that is streamable. +""" +interface Streamable { + """ + Spoken language is replaced by language of choice. + """ + dubs: [String!]! + + """ + Which regions this video is available in. + """ + regions: [String!]! + + """ + The site that is streaming this media. + """ + streamer: Streamer! + + """ + Languages this is translated to. Usually placed at bottom of media. + """ + subs: [String!]! +} + +""" +The streaming company. +""" +type Streamer implements WithTimestamps { + createdAt: ISO8601DateTime! + + id: ID! + + """ + The name of the site that is streaming this media. + """ + siteName: String! + + """ + Additional media this site is streaming. + """ + streamingLinks("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): StreamingLinkConnection! + + updatedAt: ISO8601DateTime! + + """ + Videos of the media being streamed. + """ + videos("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): VideoConnection! +} + +""" +The stream link. +""" +type StreamingLink implements Streamable & WithTimestamps { + createdAt: ISO8601DateTime! + + """ + Spoken language is replaced by language of choice. + """ + dubs: [String!]! + + id: ID! + + """ + The media being streamed + """ + media: Media! + + """ + Which regions this video is available in. + """ + regions: [String!]! + + """ + The site that is streaming this media. + """ + streamer: Streamer! + + """ + Languages this is translated to. Usually placed at bottom of media. + """ + subs: [String!]! + + updatedAt: ISO8601DateTime! + + """ + Fully qualified URL for the streaming link. + """ + url: String! +} + +""" +The connection type for StreamingLink. +""" +type StreamingLinkConnection { + """ + A list of edges. + """ + edges: [StreamingLinkEdge] + + """ + A list of nodes. + """ + nodes: [StreamingLink] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type StreamingLinkEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: StreamingLink +} + +""" +Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text. +""" +scalar String + +enum TitleLanguagePreferenceEnum { + """ + Prefer the most commonly-used title for media + """ + CANONICAL + + """ + Prefer the romanized title for media + """ + ROMANIZED + + """ + Prefer the localized title for media + """ + LOCALIZED +} + +type TitlesList { + """ + A list of additional, alternative, abbreviated, or unofficial titles + """ + alternatives: [String!] + + """ + The official or de facto international title + """ + canonical: String! + + """ + The locale code that identifies which title is used as the canonical title + """ + canonicalLocale: String + + """ + The list of localized titles keyed by locale + """ + localized(locales: [String!]): Map! + + """ + The original title of the media in the original language + """ + original: String + + """ + The locale code that identifies which title is used as the original title + """ + originalLocale: String + + """ + The title that best matches the user's preferred settings + """ + preferred: String! + + """ + The original title, romanized into latin script + """ + romanized: String + + """ + The locale code that identifies which title is used as the romanized title + """ + romanizedLocale: String + + """ + The title translated into the user's locale + """ + translated: String + + """ + The locale code that identifies which title is used as the translated title + """ + translatedLocale: String +} + +input TitlesListInput { + canonical: String + + localized: Map + + alternatives: [String!] + + canonicalLocale: String +} + +""" +Media units such as episodes or chapters +""" +interface Unit { + """ + A brief summary or description of the unit + """ + description(locales: [String!]): Map! + + id: ID! + + """ + The sequence number of this unit + """ + number: Int! + + """ + A thumbnail image for the unit + """ + thumbnail: Image + + """ + The titles for this unit in various locales + """ + titles: TitlesList! +} + +scalar Upload + +""" +The mutation failed validation. This is usually because the input provided was invalid in some way, such as a missing required field or an invalid value for a field. There may be multiple of this error, one for each failed validation, and the `path` will generally refer to a location in the input parameters, that you can map back to the input fields in your form. The recommended action is to display validation errors to the user, and allow them to correct the input and resubmit. +""" +type ValidationError implements Error { + """ + The error code. + """ + code: String + + """ + A description of the error + """ + message: String! + + """ + Which input value this error came from + """ + path: [String!] +} + +""" +The media video. +""" +type Video implements Streamable & WithTimestamps { + createdAt: ISO8601DateTime! + + """ + Spoken language is replaced by language of choice. + """ + dubs: [String!]! + + """ + The episode of this video + """ + episode: Episode! + + id: ID! + + """ + Which regions this video is available in. + """ + regions: [String!]! + + """ + The site that is streaming this media. + """ + streamer: Streamer! + + """ + Languages this is translated to. Usually placed at bottom of media. + """ + subs: [String!]! + + updatedAt: ISO8601DateTime! + + """ + The url of the video. + """ + url: String! +} + +""" +The connection type for Video. +""" +type VideoConnection { + """ + A list of edges. + """ + edges: [VideoEdge] + + """ + A list of nodes. + """ + nodes: [Video] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type VideoEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Video +} + +""" +A manga volume which can contain multiple chapters. +""" +type Volume implements Unit & WithTimestamps { + """ + The chapters in this volume. + """ + chapters("Returns the elements in the list that come after the specified cursor." after: String, "Returns the elements in the list that come before the specified cursor." before: String, "Returns the first _n_ elements from the list." first: Int, "Returns the last _n_ elements from the list." last: Int): ChapterConnection + + """ + The number of chapters in this volume. + """ + chaptersCount: Int + + createdAt: ISO8601DateTime! + + """ + A brief summary or description of the unit + """ + description(locales: [String!]): Map! + + id: ID! + + """ + The isbn number of this volume. + """ + isbn: [String!]! + + """ + The manga this volume is in. + """ + manga: Manga! + + """ + The sequence number of this unit + """ + number: Int! + + """ + The date when this chapter was released. + """ + published: ISO8601Date + + """ + A thumbnail image for the unit + """ + thumbnail: Image + + """ + The titles for this unit in various locales + """ + titles: TitlesList! + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for Volume. +""" +type VolumeConnection { + """ + A list of edges. + """ + edges: [VolumeEdge] + + """ + A list of nodes. + """ + nodes: [Volume] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +""" +An edge in a connection. +""" +type VolumeEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Volume +} + +enum VolumeSortEnum { + CREATED_AT + + UPDATED_AT + + NUMBER +} + +input VolumeSortOption { + on: VolumeSortEnum! + + direction: SortDirection! +} + +enum WaifuOrHusbandoEnum { + WAIFU + + HUSBANDO +} + +""" +A Wiki Submission is used to either create or edit existing data in our database. This will allow a simple and convient way for users to submit issues/corrections without all the work being left to the mods. +""" +type WikiSubmission implements WithTimestamps { + """ + The user who created this draft + """ + author: Profile! + + createdAt: ISO8601DateTime! + + """ + The full object that holds all the details for any modifications/additions/deletions made to the entity you are editing. This will be validated using JSON Schema. + """ + data: JSON + + id: ID! + + """ + Any additional information that may need to be provided related to the Wiki Submission + """ + notes: String + + """ + The status of the Wiki Submission + """ + status: WikiSubmissionStatusEnum! + + """ + The title given to the Wiki Submission. This will default to the title of what is being edited. + """ + title: String + + updatedAt: ISO8601DateTime! +} + +""" +The connection type for WikiSubmission. +""" +type WikiSubmissionConnection { + """ + A list of edges. + """ + edges: [WikiSubmissionEdge] + + """ + A list of nodes. + """ + nodes: [WikiSubmission] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The total amount of nodes. + """ + totalCount: Int! +} + +input WikiSubmissionCreateDraftInput { + data: JSON! + + title: String + + notes: String +} + +""" +Autogenerated return type of WikiSubmissionCreateDraft. +""" +type WikiSubmissionCreateDraftPayload { + errors: [Error!] + + wikiSubmission: WikiSubmission +} + +""" +An edge in a connection. +""" +type WikiSubmissionEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: WikiSubmission +} + +type WikiSubmissionMutations { + """ + Create a wiki submission draft + """ + createDraft("Create a wiki submission draft." input: WikiSubmissionCreateDraftInput!): WikiSubmissionCreateDraftPayload + + """ + Submit a wiki submission draft + """ + submitDraft("Submit a wiki submission draft. This will change the status to pending." input: WikiSubmissionSubmitDraftInput!): WikiSubmissionSubmitDraftPayload + + """ + Update a wiki submission draft + """ + updateDraft("Update a wiki submission draft." input: WikiSubmissionUpdateDraftInput!): WikiSubmissionUpdateDraftPayload +} + +enum WikiSubmissionSortEnum { + CREATED_AT + + UPDATED_AT +} + +input WikiSubmissionSortOption { + on: WikiSubmissionSortEnum! + + direction: SortDirection! +} + +enum WikiSubmissionStatusEnum { + """""" + DRAFT + + """""" + PENDING + + """""" + APPROVED + + """""" + REJECTED +} + +input WikiSubmissionSubmitDraftInput { + id: ID! + + data: JSON! + + title: String + + notes: String +} + +""" +Autogenerated return type of WikiSubmissionSubmitDraft. +""" +type WikiSubmissionSubmitDraftPayload { + errors: [Error!] + + wikiSubmission: WikiSubmission +} + +input WikiSubmissionUpdateDraftInput { + id: ID! + + data: JSON! + + notes: String +} + +""" +Autogenerated return type of WikiSubmissionUpdateDraft. +""" +type WikiSubmissionUpdateDraftPayload { + errors: [Error!] + + wikiSubmission: WikiSubmission +} + +interface WithTimestamps { + createdAt: ISO8601DateTime! + + updatedAt: ISO8601DateTime! +} + +scalar _Any + +union _Entity = Account|Anime|Chapter|Character|Episode|Franchise|Manga|MediaReaction|Person|Post|Producer|Profile|Quote|Review|Volume + +""" +The sdl representing the federated service capabilities. Includes federation directives, removes federation types, and includes rest of full schema after schema directives have been applied +""" +type _Service { + sdl: String +} + +""" +A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + +In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor. +""" +type __Directive { + args(includeDeprecated: Boolean = false): [__InputValue!]! + + description: String + + isRepeatable: Boolean + + locations: [__DirectiveLocation!]! + + name: String! + + onField: Boolean! @deprecated(reason: "Use `locations`.") + + onFragment: Boolean! @deprecated(reason: "Use `locations`.") + + onOperation: Boolean! @deprecated(reason: "Use `locations`.") +} + +""" +A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a fragment definition. + """ + FRAGMENT_DEFINITION + + """ + Location adjacent to a fragment spread. + """ + FRAGMENT_SPREAD + + """ + Location adjacent to an inline fragment. + """ + INLINE_FRAGMENT + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an input object field definition. + """ + INPUT_FIELD_DEFINITION + + """ + Location adjacent to a variable definition. + """ + VARIABLE_DEFINITION +} + +""" +One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. +""" +type __EnumValue { + deprecationReason: String + + description: String + + isDeprecated: Boolean! + + name: String! +} + +""" +Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. +""" +type __Field { + args(includeDeprecated: Boolean = false): [__InputValue!]! + + deprecationReason: String + + description: String + + isDeprecated: Boolean! + + name: String! + + type: __Type! +} + +""" +Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. +""" +type __InputValue { + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + + deprecationReason: String + + description: String + + isDeprecated: Boolean! + + name: String! + + type: __Type! +} + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. +""" +type __Schema { + description: String + + """ + A list of all directives supported by this server. + """ + directives: [__Directive!]! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all types supported by this server. + """ + types: [__Type!]! +} + +""" +The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. +""" +type __Type { + description: String + + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + + fields(includeDeprecated: Boolean = false): [__Field!] + + inputFields(includeDeprecated: Boolean = false): [__InputValue!] + + interfaces: [__Type!] + + isOneOf: Boolean! + + kind: __TypeKind! + + name: String + + ofType: __Type + + possibleTypes: [__Type!] + + specifiedByURL: String +} + +""" +An enum describing what kind of type a given `__Type` is. +""" +enum __TypeKind { + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + + """ + Indicates this type is an enum. `enumValues` is a valid field. + """ + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """ + Indicates this type is a list. `ofType` is a valid field. + """ + LIST + + """ + Indicates this type is a non-null. `ofType` is a valid field. + """ + NON_NULL +} + +""" +Marks an element of a GraphQL schema as no longer supported. +""" +directive @deprecated("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION + +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Requires that exactly one field must be supplied and that field must not be `null`. +""" +directive @oneOf on INPUT_OBJECT + +""" +Ensure the current user has the necessary site permission to access a resource +""" +directive @sitePermission("The required permission" required: String!) on FIELD_DEFINITION | OBJECT | SCALAR | ENUM | UNION | INTERFACE | INPUT_OBJECT | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Exposes a URL that specifies the behavior of this scalar. +""" +directive @specifiedBy("The URL that specifies the behavior of this scalar." url: String!) on SCALAR + +schema { + query: Query + mutation: Mutation +} + diff --git a/app/src/main/graphql/mihon/graphql/shikimori/MangaFragment.graphql b/app/src/main/graphql/mihon/graphql/shikimori/MangaFragment.graphql new file mode 100644 index 000000000..7eaad481d --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/shikimori/MangaFragment.graphql @@ -0,0 +1,22 @@ +fragment MangaFragment on Manga { + id + name + chapters + kind + poster { + mainUrl + } + score + url + status + airedOn { + date + } + description + personRoles { + person { + name + } + rolesEn + } +} diff --git a/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetCurrentUser.graphql b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetCurrentUser.graphql new file mode 100644 index 000000000..fad224a9e --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetCurrentUser.graphql @@ -0,0 +1,6 @@ +query ShikimoriGetCurrentUser { + currentUser { + id + nickname + } +} diff --git a/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetLibManga.graphql b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetLibManga.graphql new file mode 100644 index 000000000..9798d6d55 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetLibManga.graphql @@ -0,0 +1,19 @@ +query ShikimoriGetLibManga( + $remote_id: String!, +) { + mangas( + ids: $remote_id, + limit: 1, + ) { + id + url + name + chapters + userRate { + id + chapters + status + score + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetMangaDetails.graphql b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetMangaDetails.graphql new file mode 100644 index 000000000..f69f84711 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriGetMangaDetails.graphql @@ -0,0 +1,11 @@ +query ShikimoriGetMangaDetails( + $query: String!, +) { + mangas( + ids: $query, + limit: 1, + kind: "!light_novel,!novel", + ) { + ...MangaFragment + } +} diff --git a/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriSearchManga.graphql b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriSearchManga.graphql new file mode 100644 index 000000000..9ff0f1502 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/shikimori/ShikimoriSearchManga.graphql @@ -0,0 +1,11 @@ +query ShikimoriSearchManga( + $search: String! +) { + mangas( + search: $search, + limit: 20, + kind: "!light_novel,!novel", + ) { + ...MangaFragment + } +} diff --git a/app/src/main/graphql/mihon/graphql/shikimori/shikimori.graphqls b/app/src/main/graphql/mihon/graphql/shikimori/shikimori.graphqls new file mode 100644 index 000000000..17713f5a9 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/shikimori/shikimori.graphqls @@ -0,0 +1,1913 @@ +type Anime { + airedOn: IncompleteDate + + characterRoles: [CharacterRole!] + + chronology: [Anime!] + + createdAt: ISO8601DateTime! + + description: String + + descriptionHtml: String + + descriptionSource: String + + """ + Duration in minutes + """ + duration: Int + + english: String + + episodes: Int! + + episodesAired: Int! + + externalLinks: [ExternalLink!] + + fandubbers: [String!]! + + fansubbers: [String!]! + + """ + Franchise name + """ + franchise: String + + genres: [Genre!] + + id: ID! + + isCensored: Boolean + + japanese: String + + kind: AnimeKindEnum + + licenseNameRu: String + + licensors: [String!] + + malId: ID + + name: String! + + nextEpisodeAt: ISO8601DateTime + + opengraphImageUrl: String + + origin: AnimeOriginEnum + + personRoles: [PersonRole!] + + poster: Poster + + rating: AnimeRatingEnum + + related: [Related!] + + releasedOn: IncompleteDate + + russian: String + + score: Float + + scoresStats: [ScoreStat!] + + screenshots: [Screenshot!]! + + season: String + + status: AnimeStatusEnum + + statusesStats: [StatusStat!] + + studios: [Studio!]! + + synonyms: [String!]! + + topic: Topic + + updatedAt: ISO8601DateTime! + + url: String! + + userRate: UserRate + + videos: [Video!]! +} + +enum AnimeKindEnum { + """ + TV Series + """ + tv + + """ + Movie + """ + movie + + """ + OVA + """ + ova + + """ + ONA + """ + ona + + """ + Special + """ + special + + """ + TV Special + """ + tv_special + + """ + Music + """ + music + + """ + PV + """ + pv + + """ + CM + """ + cm +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`movie` - Movies + +`music` - Music + +`ona` - ONA + +`ova/ona` - OVA/ONA + +`ova` - OVA + +`special` - Specials + +`tv` - TV Series + +`tv_13` - Short TV Series + +`tv_24` - Average TV Series + +`tv_48` - Long TV Series + +`tv_special` - TV Specials + +`pv` - Promotional Videos + +`cm` - Commercial Messages +""" +scalar AnimeKindString + +enum AnimeOriginEnum { + """ + Original + """ + original + + """ + Manga + """ + manga + + """ + Web manga + """ + web_manga + + """ + 4-koma manga + """ + four_koma_manga + + """ + Novel + """ + novel + + """ + Web novel + """ + web_novel + + """ + Visual novel + """ + visual_novel + + """ + Light novel + """ + light_novel + + """ + Game + """ + game + + """ + Card game + """ + card_game + + """ + Music + """ + music + + """ + Radio + """ + radio + + """ + Book + """ + book + + """ + Picture book + """ + picture_book + + """ + Mixed media + """ + mixed_media + + """ + Other + """ + other + + """ + Unknown + """ + unknown +} + +enum AnimeRatingEnum { + """ + None + """ + none + + """ + G + """ + g + + """ + PG + """ + pg + + """ + PG-13 + """ + pg_13 + + """ + R-17 + """ + r + + """ + R+ + """ + r_plus + + """ + Rx + """ + rx +} + +enum AnimeStatusEnum { + """ + Planned + """ + anons + + """ + Airing + """ + ongoing + + """ + Released + """ + released +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`anons` - Planned + +`ongoing` - Airing + +`released` - Released +""" +scalar AnimeStatusString + +""" +Represents `true` or `false` values. +""" +scalar Boolean + +type Character { + createdAt: ISO8601DateTime! + + description: String + + descriptionHtml: String + + descriptionSource: String + + id: ID! + + isAnime: Boolean! + + isManga: Boolean! + + isRanobe: Boolean! + + japanese: String + + malId: ID + + name: String! + + poster: Poster + + russian: String + + synonyms: [String!]! + + topic: Topic + + updatedAt: ISO8601DateTime! + + url: String! +} + +type CharacterRole { + character: Character! + + id: ID! + + rolesEn: [String!]! + + rolesRu: [String!]! +} + +type Contest { + description: String + + descriptionHtml: String + + descriptionSource: String + + finishedOn: ISO8601Date + + id: ID! + + matchDuration: Int + + matchesInterval: Int + + matchesPerRound: Int + + memberType: ContestMemberTypeEnum! + + name: String! + + rounds: [ContestRound!]! + + startedOn: ISO8601Date + + state: ContestStateEnum! + + strategyType: ContestStrategyTypeEnum! +} + +type ContestMatch { + id: ID! + + leftAnime: Anime + + leftCharacter: Character + + leftId: Int + + leftVotes: Int + + rightAnime: Anime + + rightCharacter: Character + + rightId: Int + + rightVotes: Int + + state: ContestMatchStateEnum! + + winnerId: Int +} + +enum ContestMatchStateEnum { + """ + created + """ + created + + """ + started + """ + started + + """ + freezed + """ + freezed + + """ + finished + """ + finished +} + +enum ContestMemberTypeEnum { + """ + Anime + """ + anime + + """ + Characters + """ + character +} + +type ContestRound { + id: ID! + + isAdditional: Boolean! + + matches: [ContestMatch!]! + + name: String! + + number: Int! + + state: ContestRoundStateEnum! +} + +enum ContestRoundStateEnum { + """ + created + """ + created + + """ + started + """ + started + + """ + finished + """ + finished +} + +enum ContestStateEnum { + """ + created + """ + created + + """ + proposing + """ + proposing + + """ + started + """ + started + + """ + finished + """ + finished +} + +enum ContestStrategyTypeEnum { + """ + Double elimination + """ + double_elimination + + """ + Olympic system + """ + play_off + + """ + Swiss system + """ + swiss +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`S` - Less than 10 minutes + +`D` - Less than 30 minutes + +`F` - More than 30 minutes +""" +scalar DurationString + +type ExternalLink { + createdAt: ISO8601DateTime + + id: ID + + kind: ExternalLinkKindEnum! + + updatedAt: ISO8601DateTime + + url: String! +} + +enum ExternalLinkKindEnum { + """ + Official Site + """ + official_site + + """ + Wikipedia + """ + wikipedia + + """ + Anime News Network + """ + anime_news_network + + """ + MyAnimeList + """ + myanimelist + + """ + AniDB + """ + anime_db + + """ + World Art + """ + world_art + + """ + KinoPoisk + """ + kinopoisk + + """ + Kage Project + """ + kage_project + + """ + Twitter/X + """ + twitter + + """ + Anime 365 + """ + smotret_anime + + """ + Shikimori + """ + shiki + + """ + Amediateka + """ + amediateka + + """ + Crunchyroll + """ + crunchyroll + + """ + Amazon + """ + amazon + + """ + Hidive + """ + hidive + + """ + Hulu + """ + hulu + + """ + Ivi + """ + ivi + + """ + KinoPoisk HD + """ + kinopoisk_hd + + """ + Wink + """ + wink + + """ + Netflix + """ + netflix + + """ + Okko + """ + okko + + """ + Youtube + """ + youtube + + """ + ReManga + """ + remanga + + """ + Senkuro + """ + senkuro + + """ + MangaLib + """ + mangalib + + """ + Baka-Updates + """ + mangaupdates + + """ + ReadManga + """ + readmanga + + """ + MangaDex + """ + mangadex + + """ + MangaFox + """ + mangafox + + """ + Mangachan + """ + mangachan + + """ + Mangahub + """ + mangahub + + """ + Novel.tl + """ + novel_tl + + """ + RuRanobe + """ + ruranobe + + """ + RanobeLib + """ + ranobelib + + """ + Novel Updates + """ + novelupdates +} + +""" +Represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point). +""" +scalar Float + +type Genre { + entryType: GenreEntryTypeEnum! + + id: ID! + + kind: GenreKindEnum! + + name: String! + + russian: String! +} + +enum GenreEntryTypeEnum { + """ + Anime + """ + Anime + + """ + Manga + """ + Manga +} + +enum GenreKindEnum { + """ + Demographic + """ + demographic + + """ + Genre + """ + genre + + """ + Theme + """ + theme +} + +""" +Represents a unique identifier that is Base64 obfuscated. It is often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"VXNlci0xMA=="`) or integer (such as `4`) input value will be accepted as an ID. +""" +scalar ID + +""" +An ISO 8601-encoded date +""" +scalar ISO8601Date + +""" +An ISO 8601-encoded datetime +""" +scalar ISO8601DateTime + +type IncompleteDate { + date: ISO8601Date + + day: Int + + month: Int + + year: Int +} + +""" +Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1. +""" +scalar Int + +type Manga { + airedOn: IncompleteDate + + chapters: Int! + + characterRoles: [CharacterRole!] + + chronology: [Manga!] + + createdAt: ISO8601DateTime! + + description: String + + descriptionHtml: String + + descriptionSource: String + + english: String + + externalLinks: [ExternalLink!] + + """ + Franchise name + """ + franchise: String + + genres: [Genre!] + + id: ID! + + isCensored: Boolean + + japanese: String + + kind: MangaKindEnum + + licenseNameRu: String + + licensors: [String!] + + malId: ID + + name: String! + + opengraphImageUrl: String + + personRoles: [PersonRole!] + + poster: Poster + + publishers: [Publisher!]! + + related: [Related!] + + releasedOn: IncompleteDate + + russian: String + + score: Float + + scoresStats: [ScoreStat!] + + status: MangaStatusEnum + + statusesStats: [StatusStat!] + + synonyms: [String!]! + + topic: Topic + + updatedAt: ISO8601DateTime! + + url: String! + + userRate: UserRate + + volumes: Int! +} + +enum MangaKindEnum { + """ + Manga + """ + manga + + """ + Manhwa + """ + manhwa + + """ + Manhua + """ + manhua + + """ + Light Novel + """ + light_novel + + """ + Novel + """ + novel + + """ + One Shot + """ + one_shot + + """ + Doujin + """ + doujin +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`doujin` - Doujin + +`manga` - Manga + +`manhua` - Manhua + +`manhwa` - Manhwa + +`light_novel` - Light Novels + +`novel` - Novels + +`one_shot` - One Shots +""" +scalar MangaKindString + +enum MangaStatusEnum { + """ + Planned + """ + anons + + """ + Publishing + """ + ongoing + + """ + Published + """ + released + + """ + Paused + """ + paused + + """ + Discontinued + """ + discontinued +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`anons` - Planned + +`ongoing` - Publishing + +`released` - Published + +`paused` - Paused + +`discontinued` - Discontinued +""" +scalar MangaStatusString + +type Mutation { + """ + An example field added by the generator + """ + testField: String! +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`planned` - Planned to Watch + +`watching` - Watching + +`rewatching` - Rewatching + +`completed` - Completed + +`on_hold` - On Hold + +`dropped` - Dropped +""" +scalar MylistString + +enum OrderEnum { + """ + By ID + """ + id + + """ + id_desc + """ + id_desc + + """ + By rank + """ + ranked + + """ + By type + """ + kind + + """ + By popularity + """ + popularity + + """ + In alphabetical order + """ + name + + """ + By release date + """ + aired_on + + """ + By number of episodes + """ + episodes + + """ + By status + """ + status + + """ + By random + """ + random + + """ + By random + """ + ranked_random + + """ + By Shikimori ranking + """ + ranked_shiki + + """ + created_at + """ + created_at + + """ + created_at_desc + """ + created_at_desc + + """ + By field updated_at on entity + """ + updated_at + + """ + updated_at_desc + """ + updated_at_desc +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`card_game` - Card game + +`novel` - Novel + +`radio` - Radio + +`game` - Game + +`unknown` - Unknown + +`book` - Book + +`light_novel` - Light novel + +`web_novel` - Web novel + +`original` - Original + +`picture_book` - Picture book + +`music` - Music + +`manga` - Manga + +`visual_novel` - Visual novel + +`other` - Other + +`web_manga` - Web manga + +`four_koma_manga` - 4-koma manga + +`mixed_media` - Mixed media +""" +scalar OriginString + +type Person { + birthOn: IncompleteDate + + createdAt: ISO8601DateTime! + + deceasedOn: IncompleteDate + + id: ID! + + isMangaka: Boolean! + + isProducer: Boolean! + + isSeyu: Boolean! + + japanese: String + + malId: ID + + name: String! + + poster: Poster + + russian: String + + synonyms: [String!]! + + topic: Topic + + updatedAt: ISO8601DateTime! + + url: String! + + website: String +} + +type PersonRole { + id: ID! + + person: Person! + + rolesEn: [String!]! + + rolesRu: [String!]! +} + +""" +A positive integer (>= 1) +""" +scalar PositiveInt + +type Poster { + id: ID! + + main2xUrl: String! + + mainAlt2xUrl: String! + + mainAltUrl: String! + + mainUrl: String! + + mini2xUrl: String! + + miniAlt2xUrl: String! + + miniAltUrl: String! + + miniUrl: String! + + originalUrl: String! + + preview2xUrl: String! + + previewAlt2xUrl: String! + + previewAltUrl: String! + + previewUrl: String! +} + +type Publisher { + id: ID! + + name: String! +} + +type Query { + animes(page: PositiveInt = 1, "Maximum 50" limit: PositiveInt = 2, order: OrderEnum = ranked, kind: AnimeKindString, status: AnimeStatusString, season: SeasonString, "Minimal anime score" score: Int, duration: DurationString, rating: RatingString, origin: OriginString, "List of comma separated genre ids" genre: String, "List of comma separated studio ids" studio: String, "List of comma separated franchises" franchise: String, "Set to `false` to allow hentai, yaoi and yuri" censored: Boolean, mylist: MylistString, "List of comma separated ids" ids: String, "List of comma separated ids" excludeIds: String, search: String): [Anime!]! + + characters(page: PositiveInt = 1, "Maximum 50" limit: PositiveInt = 2, ids: String, search: String): [Character!]! + + contests(page: PositiveInt = 1, "Maximum 10" limit: PositiveInt = 2, ids: [ID!]): [Contest!]! + + currentUser: User + + genres(entryType: GenreEntryTypeEnum!): [Genre!]! + + mangas(page: PositiveInt = 1, "Maximum 50" limit: PositiveInt = 2, order: OrderEnum = ranked, kind: MangaKindString, status: MangaStatusString, season: SeasonString, "Minimal manga score" score: Int, "List of comma separated genre ids" genre: String, "List of comma separated publisher ids" publisher: String, "List of comma separated franchises" franchise: String, "Set to `false` to allow hentai, yaoi and yuri" censored: Boolean, mylist: MylistString, "List of comma separated ids" ids: String, "List of comma separated ids" excludeIds: String, search: String): [Manga!]! + + people(page: PositiveInt = 1, "Maximum 50" limit: PositiveInt = 2, ids: [ID!], search: String, isSeyu: Boolean, isProducer: Boolean, isMangaka: Boolean): [Person!]! + + userRates(page: PositiveInt = 1, "Maximum 50" limit: PositiveInt = 2, "ID of current user is used by default" userId: ID, targetType: UserRateTargetTypeEnum, status: UserRateStatusEnum, order: UserRateOrderInputType): [UserRate!]! + + users(page: PositiveInt = 1, "Maximum 50" limit: PositiveInt = 2, ids: [ID!], search: String): [User!]! +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +`none` - No rating + +`g` - G - All ages + +`pg` - PG - Children + +`pg_13` - PG-13 - Teens 13 or older + +`r` - R - 17+ recommended (violence & profanity) + +`r_plus` - R+ - Mild Nudity (may also contain violence & profanity) + +`rx` - Rx - Hentai (extreme sexual content/nudity) +""" +scalar RatingString + +type Related { + anime: Anime + + id: ID! + + manga: Manga + + relationEn: String! @deprecated(reason: "use relation_kind/relation_text instead. This field will be deleted after 2025-01-01") + + relationKind: RelationKindEnum! + + relationRu: String! @deprecated(reason: "use relation_kind/relation_text instead. This field will be deleted after 2025-01-01") + + relationText: String! +} + +enum RelationKindEnum { + """ + Adaptation + """ + adaptation + + """ + Alternative Setting + """ + alternative_setting + + """ + Alternative Version + """ + alternative_version + + """ + Character + """ + character + + """ + Full Story + """ + full_story + + """ + Other + """ + other + + """ + Parent Story + """ + parent_story + + """ + Prequel + """ + prequel + + """ + Sequel + """ + sequel + + """ + Side Story + """ + side_story + + """ + Spin-off + """ + spin_off + + """ + Summary + """ + summary +} + +type ScoreStat { + count: Int! + + score: Int! +} + +type Screenshot { + id: ID! + + originalUrl: String! + + x166Url: String! + + x332Url: String! +} + +""" +List of values separated by comma. +Add `!` before value to apply negative filter. + + +**Examples:** + +`summer_2017` + +`2016` + +`2014_2016` + +`199x` +""" +scalar SeasonString + +enum SortOrderEnum { + """ + Sort in ascending order + """ + asc + + """ + Sort in descending order + """ + desc +} + +type StatusStat { + count: Int! + + status: UserRateStatusEnum! +} + +""" +Represents textual data as UTF-8 character sequences. This type is most often used by GraphQL to represent free-form human-readable text. +""" +scalar String + +type Studio { + id: ID! + + imageUrl: String + + name: String! +} + +type Topic { + body: String! + + commentsCount: Int! + + createdAt: ISO8601DateTime! + + htmlBody: String! + + id: ID + + tags: [String!]! + + title: String! + + type: String! + + updatedAt: ISO8601DateTime! + + url: String! +} + +type User { + avatarUrl: String! + + id: ID! + + lastOnlineAt: ISO8601DateTime! + + nickname: String! + + url: String! +} + +type UserRate { + anime: Anime + + chapters: Int! + + createdAt: ISO8601DateTime! + + episodes: Int! + + id: ID! + + manga: Manga + + rewatches: Int! + + score: Int! + + status: UserRateStatusEnum! + + text: String + + updatedAt: ISO8601DateTime! + + volumes: Int! +} + +enum UserRateOrderFieldEnum { + """ + By id + """ + id + + """ + By updated_at + """ + updated_at +} + +input UserRateOrderInputType { + field: UserRateOrderFieldEnum! + + order: SortOrderEnum! +} + +enum UserRateStatusEnum { + """ + Planned to Watch + """ + planned + + """ + Watching + """ + watching + + """ + Rewatching + """ + rewatching + + """ + Completed + """ + completed + + """ + On Hold + """ + on_hold + + """ + Dropped + """ + dropped +} + +enum UserRateTargetTypeEnum { + """ + Anime + """ + Anime + + """ + Manga + """ + Manga +} + +type Video { + id: ID! + + imageUrl: String! + + kind: VideoKindEnum! + + name: String + + playerUrl: String! + + url: String! +} + +enum VideoKindEnum { + """ + PV + """ + pv + + """ + Character trailer + """ + character_trailer + + """ + CM + """ + cm + + """ + OP + """ + op + + """ + ED + """ + ed + + """ + Music + """ + op_ed_clip + + """ + Clip + """ + clip + + """ + Other + """ + other + + """ + Episode preview + """ + episode_preview +} + +""" +A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. + +In some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor. +""" +type __Directive { + args(includeDeprecated: Boolean = false): [__InputValue!]! + + description: String + + isRepeatable: Boolean + + locations: [__DirectiveLocation!]! + + name: String! + + onField: Boolean! @deprecated(reason: "Use `locations`.") + + onFragment: Boolean! @deprecated(reason: "Use `locations`.") + + onOperation: Boolean! @deprecated(reason: "Use `locations`.") +} + +""" +A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. +""" +enum __DirectiveLocation { + """ + Location adjacent to a query operation. + """ + QUERY + + """ + Location adjacent to a mutation operation. + """ + MUTATION + + """ + Location adjacent to a subscription operation. + """ + SUBSCRIPTION + + """ + Location adjacent to a field. + """ + FIELD + + """ + Location adjacent to a fragment definition. + """ + FRAGMENT_DEFINITION + + """ + Location adjacent to a fragment spread. + """ + FRAGMENT_SPREAD + + """ + Location adjacent to an inline fragment. + """ + INLINE_FRAGMENT + + """ + Location adjacent to a schema definition. + """ + SCHEMA + + """ + Location adjacent to a scalar definition. + """ + SCALAR + + """ + Location adjacent to an object type definition. + """ + OBJECT + + """ + Location adjacent to a field definition. + """ + FIELD_DEFINITION + + """ + Location adjacent to an argument definition. + """ + ARGUMENT_DEFINITION + + """ + Location adjacent to an interface definition. + """ + INTERFACE + + """ + Location adjacent to a union definition. + """ + UNION + + """ + Location adjacent to an enum definition. + """ + ENUM + + """ + Location adjacent to an enum value definition. + """ + ENUM_VALUE + + """ + Location adjacent to an input object type definition. + """ + INPUT_OBJECT + + """ + Location adjacent to an input object field definition. + """ + INPUT_FIELD_DEFINITION + + """ + Location adjacent to a variable definition. + """ + VARIABLE_DEFINITION +} + +""" +One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. +""" +type __EnumValue { + deprecationReason: String + + description: String + + isDeprecated: Boolean! + + name: String! +} + +""" +Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type. +""" +type __Field { + args(includeDeprecated: Boolean = false): [__InputValue!]! + + deprecationReason: String + + description: String + + isDeprecated: Boolean! + + name: String! + + type: __Type! +} + +""" +Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value. +""" +type __InputValue { + """ + A GraphQL-formatted string representing the default value for this input value. + """ + defaultValue: String + + deprecationReason: String + + description: String + + isDeprecated: Boolean! + + name: String! + + type: __Type! +} + +""" +A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. +""" +type __Schema { + description: String + + """ + A list of all directives supported by this server. + """ + directives: [__Directive!]! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type + + """ + A list of all types supported by this server. + """ + types: [__Type!]! +} + +""" +The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum. + +Depending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name and description, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types. +""" +type __Type { + description: String + + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + + fields(includeDeprecated: Boolean = false): [__Field!] + + inputFields(includeDeprecated: Boolean = false): [__InputValue!] + + interfaces: [__Type!] + + isOneOf: Boolean! + + kind: __TypeKind! + + name: String + + ofType: __Type + + possibleTypes: [__Type!] + + specifiedByURL: String +} + +""" +An enum describing what kind of type a given `__Type` is. +""" +enum __TypeKind { + """ + Indicates this type is a scalar. + """ + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + + """ + Indicates this type is an enum. `enumValues` is a valid field. + """ + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """ + Indicates this type is a list. `ofType` is a valid field. + """ + LIST + + """ + Indicates this type is a non-null. `ofType` is a valid field. + """ + NON_NULL +} + +""" +Marks an element of a GraphQL schema as no longer supported. +""" +directive @deprecated("Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted in [Markdown](https://daringfireball.net/projects/markdown/)." reason: String = "No longer supported") on FIELD_DEFINITION | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION + +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Requires that exactly one field must be supplied and that field must not be `null`. +""" +directive @oneOf on INPUT_OBJECT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Exposes a URL that specifies the behavior of this scalar. +""" +directive @specifiedBy("The URL that specifies the behavior of this scalar." url: String!) on SCALAR + +schema { + query: Query + mutation: Mutation +} + diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/MangaFragment.graphql b/app/src/main/graphql/mihon/graphql/suwayomi/MangaFragment.graphql new file mode 100644 index 000000000..d3b1711ff --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/MangaFragment.graphql @@ -0,0 +1,15 @@ +fragment MangaFragment on MangaType { + description + id + status + thumbnailUrl + title + url + chapters { + totalCount + } + latestReadChapter { + chapterNumber + } + unreadCount +} diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetManga.graphql b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetManga.graphql new file mode 100644 index 000000000..855f81bc5 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetManga.graphql @@ -0,0 +1,9 @@ +query SuwayomiGetManga( + $mangaId: Int!, +) { + manga( + id: $mangaId, + ) { + ...MangaFragment + } +} diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetMangaUnreadChapters.graphql b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetMangaUnreadChapters.graphql new file mode 100644 index 000000000..54f0ac689 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiGetMangaUnreadChapters.graphql @@ -0,0 +1,30 @@ +query SuwayomiGetMangaUnreadChapters( + $mangaId: Int!, + $chapterNumber: Float!, +) { + chapters( + condition: { + mangaId: $mangaId, + isRead: false, + }, + filter: { + and: [ + { + chapterNumber: { + lessThanOrEqualTo: $chapterNumber + } + }, + { + chapterNumber: { + greaterThan: 0 + } + }, + ] + }, + ){ + nodes { + id + chapterNumber + } + } +} diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkChaptersRead.graphql b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkChaptersRead.graphql new file mode 100644 index 000000000..2b40fec6f --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkChaptersRead.graphql @@ -0,0 +1,14 @@ +mutation SuwayomiMarkChaptersRead( + $chapters: [Int!]!, +) { + updateChapters( + input: { + ids: $chapters, + patch: { + isRead: true, + }, + }, + ) { + __typename + } +} diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkReadAndDeleteChapters.graphql b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkReadAndDeleteChapters.graphql new file mode 100644 index 000000000..a8123ec84 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiMarkReadAndDeleteChapters.graphql @@ -0,0 +1,22 @@ +mutation SuwayomiMarkAndDeleteChapters( + $chapters: [Int!]!, +) { + updateChapters( + input: { + ids: $chapters, + patch: { + isRead: true, + }, + }, + ) { + __typename + } + + deleteDownloadedChapters( + input: { + ids: $chapters, + }, + ) { + __typename + } +} diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiUpdateMangaProgress.graphql b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiUpdateMangaProgress.graphql new file mode 100644 index 000000000..6a12c0c5e --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/SuwayomiUpdateMangaProgress.graphql @@ -0,0 +1,11 @@ +mutation SuwayomiUpdateMangaProgress( + $mangaId: Int!, +) { + trackProgress( + input: { + mangaId: $mangaId, + }, + ) { + __typename + } +} diff --git a/app/src/main/graphql/mihon/graphql/suwayomi/suwayomi.graphqls b/app/src/main/graphql/mihon/graphql/suwayomi/suwayomi.graphqls new file mode 100644 index 000000000..21ebe1ff4 --- /dev/null +++ b/app/src/main/graphql/mihon/graphql/suwayomi/suwayomi.graphqls @@ -0,0 +1,4883 @@ +type AboutServerPayload { + buildTime: LongString! + + buildType: String! + + discord: String! + + github: String! + + name: String! + + revision: String! @deprecated(reason: "The version includes the revision as the patch number") + + version: String! +} + +type AboutWebUI { + channel: WebUIChannel! + + tag: String! + + updateTimestamp: LongString! +} + +input AddExtensionStoreInput { + clientMutationId: String + + indexUrl: String! +} + +type AddExtensionStorePayload { + clientMutationId: String + + extensionStore: ExtensionStoreType! +} + +enum AuthMode { + NONE + + BASIC_AUTH + + SIMPLE_LOGIN + + UI_LOGIN +} + +enum BackupRestoreState { + IDLE + + SUCCESS + + FAILURE + + RESTORING_CATEGORIES + + RESTORING_MANGA + + RESTORING_META + + RESTORING_SETTINGS +} + +type BackupRestoreStatus { + mangaProgress: Int! + + state: BackupRestoreState! + + totalManga: Int! +} + +input BindTrackInput { + clientMutationId: String + + mangaId: Int! + + """ + This will only work if the tracker of the track record supports private tracking + """ + private: Boolean + + remoteId: LongString! + + trackerId: Int! +} + +type BindTrackPayload { + clientMutationId: String + + trackRecord: TrackRecordType! +} + +input BindTrackRecordInput { + clientMutationId: String + + mangaId: Int! + + trackRecordId: Int! +} + +type BindTrackRecordPayload { + clientMutationId: String + + trackRecord: TrackRecordType! +} + +""" +Built-in Boolean +""" +scalar Boolean + +input BooleanFilterInput { + distinctFrom: Boolean + + distinctFromAll: [Boolean!] + + distinctFromAny: [Boolean!] + + equalTo: Boolean + + greaterThan: Boolean + + greaterThanOrEqualTo: Boolean + + in: [Boolean!] + + isNull: Boolean + + lessThan: Boolean + + lessThanOrEqualTo: Boolean + + notDistinctFrom: Boolean + + notEqualTo: Boolean + + notEqualToAll: [Boolean!] + + notEqualToAny: [Boolean!] + + notIn: [Boolean!] +} + +input CategoryConditionInput { + default: Boolean + + id: Int + + name: String + + order: Int +} + +type CategoryEdge implements Edge { + cursor: Cursor! + + node: CategoryType! +} + +input CategoryFilterInput { + and: [CategoryFilterInput!] + + default: BooleanFilterInput + + id: IntFilterInput + + name: StringFilterInput + + not: CategoryFilterInput + + or: [CategoryFilterInput!] + + order: IntFilterInput +} + +enum CategoryJobStatus { + UPDATING + + SKIPPED +} + +type CategoryMetaType implements MetaType { + categoryId: Int! + + key: String! + + value: String! + + category: CategoryType! +} + +input CategoryMetaTypeInput { + categoryId: Int! + + key: String! + + value: String! +} + +type CategoryNodeList implements NodeList { + edges: [CategoryEdge!]! + + nodes: [CategoryType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum CategoryOrderBy { + ID + + NAME + + ORDER +} + +input CategoryOrderInput { + by: CategoryOrderBy! + + byType: SortOrder +} + +type CategoryType { + default: Boolean! + + id: Int! + + includeInDownload: IncludeOrExclude! + + includeInUpdate: IncludeOrExclude! + + name: String! + + order: Int! + + mangas: MangaNodeList! + + meta: [CategoryMetaType!]! +} + +type CategoryUpdateType { + category: CategoryType! + + status: CategoryJobStatus! +} + +enum CbzMediaType { + MODERN + + LEGACY + + COMPATIBLE +} + +input ChapterConditionInput { + chapterNumber: Float + + fetchedAt: LongString + + id: Int + + isBookmarked: Boolean + + isDownloaded: Boolean + + isRead: Boolean + + lastPageRead: Int + + lastReadAt: LongString + + mangaId: Int + + name: String + + pageCount: Int + + realUrl: String + + scanlator: String + + sourceOrder: Int + + uploadDate: LongString + + url: String +} + +type ChapterEdge implements Edge { + cursor: Cursor! + + node: ChapterType! +} + +input ChapterFilterInput { + and: [ChapterFilterInput!] + + chapterNumber: DoubleFilterInput + + fetchedAt: LongFilterInput + + id: IntFilterInput + + inLibrary: BooleanFilterInput + + isBookmarked: BooleanFilterInput + + isDownloaded: BooleanFilterInput + + isRead: BooleanFilterInput + + lastPageRead: IntFilterInput + + lastReadAt: LongFilterInput + + mangaId: IntFilterInput + + name: StringFilterInput + + not: ChapterFilterInput + + or: [ChapterFilterInput!] + + pageCount: IntFilterInput + + realUrl: StringFilterInput + + scanlator: StringFilterInput + + sourceOrder: IntFilterInput + + uploadDate: LongFilterInput + + url: StringFilterInput +} + +type ChapterMetaType implements MetaType { + chapterId: Int! + + key: String! + + value: String! + + chapter: ChapterType! +} + +input ChapterMetaTypeInput { + chapterId: Int! + + key: String! + + value: String! +} + +type ChapterNodeList implements NodeList { + edges: [ChapterEdge!]! + + nodes: [ChapterType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum ChapterOrderBy { + ID + + SOURCE_ORDER + + NAME + + UPLOAD_DATE + + CHAPTER_NUMBER + + LAST_READ_AT + + FETCHED_AT +} + +input ChapterOrderInput { + by: ChapterOrderBy! + + byType: SortOrder +} + +type ChapterType { + chapterNumber: Float! + + fetchedAt: LongString! + + id: Int! + + isBookmarked: Boolean! + + isDownloaded: Boolean! + + isRead: Boolean! + + lastPageRead: Int! + + lastReadAt: LongString! + + mangaId: Int! + + name: String! + + pageCount: Int! + + realUrl: String + + scanlator: String + + sourceOrder: Int! + + uploadDate: LongString! + + url: String! + + manga: MangaType! + + meta: [ChapterMetaType!]! +} + +type CheckBoxFilter { + default: Boolean! + + name: String! +} + +type CheckBoxPreference { + currentValue: Boolean + + default: Boolean! + + enabled: Boolean! + + key: String + + summary: String + + title: String + + visible: Boolean! +} + +type CheckForServerUpdatesPayload { + channel: String! + + tag: String! + + url: String! +} + +input ClearCachedImagesInput { + cachedPages: Boolean + + cachedThumbnails: Boolean + + clientMutationId: String + + downloadedThumbnails: Boolean +} + +type ClearCachedImagesPayload { + cachedPages: Boolean + + cachedThumbnails: Boolean + + clientMutationId: String + + downloadedThumbnails: Boolean +} + +input ClearDownloaderInput { + clientMutationId: String +} + +type ClearDownloaderPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input ConnectKoSyncAccountInput { + clientMutationId: String + + password: String! + + serverAddress: String! + + username: String! +} + +enum ContentWarning { + SAFE + + MIXED + + NSFW +} + +input ContentWarningFilterInput { + distinctFrom: ContentWarning + + distinctFromAll: [ContentWarning!] + + distinctFromAny: [ContentWarning!] + + equalTo: ContentWarning + + greaterThan: ContentWarning + + greaterThanOrEqualTo: ContentWarning + + in: [ContentWarning!] + + isNull: Boolean + + lessThan: ContentWarning + + lessThanOrEqualTo: ContentWarning + + notDistinctFrom: ContentWarning + + notEqualTo: ContentWarning + + notEqualToAll: [ContentWarning!] + + notEqualToAny: [ContentWarning!] + + notIn: [ContentWarning!] +} + +input CreateBackupInput { + clientMutationId: String + + flags: PartialBackupFlagsInput + + includeCategories: Boolean @deprecated(reason: "Will get removed, replace with flags") + + includeChapters: Boolean @deprecated(reason: "Will get removed, replace with flags") + + includeClientData: Boolean @deprecated(reason: "Will get removed, replace with flags") + + includeHistory: Boolean @deprecated(reason: "Will get removed, replace with flags") + + includeServerSettings: Boolean @deprecated(reason: "Will get removed, replace with flags") + + includeTracking: Boolean @deprecated(reason: "Will get removed, replace with flags") +} + +type CreateBackupPayload { + clientMutationId: String + + url: String! +} + +input CreateCategoryInput { + clientMutationId: String + + default: Boolean + + includeInDownload: IncludeOrExclude + + includeInUpdate: IncludeOrExclude + + name: String! + + order: Int +} + +type CreateCategoryPayload { + category: CategoryType! + + clientMutationId: String +} + +""" +A location in a connection that can be used for resuming pagination. +""" +scalar Cursor + +enum DatabaseType { + H2 + + POSTGRESQL +} + +input DeleteCategoryInput { + categoryId: Int! + + clientMutationId: String +} + +input DeleteCategoryMetaInput { + categoryId: Int! + + clientMutationId: String + + key: String! +} + +type DeleteCategoryMetaPayload { + category: CategoryType! + + clientMutationId: String + + meta: CategoryMetaType +} + +input DeleteCategoryMetasInput { + clientMutationId: String + + items: [DeleteCategoryMetasItemInput!]! +} + +input DeleteCategoryMetasItemInput { + categoryIds: [Int!]! + + keys: [String!] + + prefixes: [String!] +} + +type DeleteCategoryMetasPayload { + categories: [CategoryType!]! + + clientMutationId: String + + metas: [CategoryMetaType!]! +} + +type DeleteCategoryPayload { + category: CategoryType + + clientMutationId: String + + mangas: [MangaType!]! +} + +input DeleteChapterMetaInput { + chapterId: Int! + + clientMutationId: String + + key: String! +} + +type DeleteChapterMetaPayload { + chapter: ChapterType! + + clientMutationId: String + + meta: ChapterMetaType +} + +input DeleteChapterMetasInput { + clientMutationId: String + + items: [DeleteChapterMetasItemInput!]! +} + +input DeleteChapterMetasItemInput { + chapterIds: [Int!]! + + keys: [String!] + + prefixes: [String!] +} + +type DeleteChapterMetasPayload { + chapters: [ChapterType!]! + + clientMutationId: String + + metas: [ChapterMetaType!]! +} + +input DeleteDownloadedChapterInput { + clientMutationId: String + + id: Int! +} + +type DeleteDownloadedChapterPayload { + chapters: ChapterType! + + clientMutationId: String +} + +input DeleteDownloadedChaptersInput { + clientMutationId: String + + ids: [Int!]! +} + +type DeleteDownloadedChaptersPayload { + chapters: [ChapterType!]! + + clientMutationId: String +} + +input DeleteGlobalMetaInput { + clientMutationId: String + + key: String! +} + +type DeleteGlobalMetaPayload { + clientMutationId: String + + meta: GlobalMetaType +} + +input DeleteGlobalMetasInput { + clientMutationId: String + + keys: [String!] + + prefixes: [String!] +} + +type DeleteGlobalMetasPayload { + clientMutationId: String + + metas: [GlobalMetaType!]! +} + +input DeleteMangaMetaInput { + clientMutationId: String + + key: String! + + mangaId: Int! +} + +type DeleteMangaMetaPayload { + clientMutationId: String + + manga: MangaType! + + meta: MangaMetaType +} + +input DeleteMangaMetasInput { + clientMutationId: String + + items: [DeleteMangaMetasItemInput!]! +} + +input DeleteMangaMetasItemInput { + keys: [String!] + + mangaIds: [Int!]! + + prefixes: [String!] +} + +type DeleteMangaMetasPayload { + clientMutationId: String + + mangas: [MangaType!]! + + metas: [MangaMetaType!]! +} + +input DeleteSourceMetaInput { + clientMutationId: String + + key: String! + + sourceId: LongString! +} + +type DeleteSourceMetaPayload { + clientMutationId: String + + meta: SourceMetaType + + source: SourceType +} + +input DeleteSourceMetasInput { + clientMutationId: String + + items: [DeleteSourceMetasItemInput!]! +} + +input DeleteSourceMetasItemInput { + keys: [String!] + + prefixes: [String!] + + sourceIds: [LongString!]! +} + +type DeleteSourceMetasPayload { + clientMutationId: String + + metas: [SourceMetaType!]! + + sources: [SourceType!]! +} + +input DequeueChapterDownloadInput { + clientMutationId: String + + id: Int! +} + +type DequeueChapterDownloadPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input DequeueChapterDownloadsInput { + clientMutationId: String + + ids: [Int!]! +} + +type DequeueChapterDownloadsPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input DoubleFilterInput { + distinctFrom: Float + + distinctFromAll: [Float!] + + distinctFromAny: [Float!] + + equalTo: Float + + greaterThan: Float + + greaterThanOrEqualTo: Float + + in: [Float!] + + isNull: Boolean + + lessThan: Float + + lessThanOrEqualTo: Float + + notDistinctFrom: Float + + notEqualTo: Float + + notEqualToAll: [Float!] + + notEqualToAny: [Float!] + + notIn: [Float!] +} + +input DownloadChangedInput { + """ + Sets a max number of updates that can be contained in a download update message.Everything above this limit will be omitted and the "downloadStatus" should be re-fetched via the corresponding query. Due to the graphql subscription execution strategy not supporting batching for data loaders, the data loaders run into the n+1 problem, which can cause the server to get unresponsive until the status update has been handled. This is an issue e.g. when mass en- or dequeuing downloads. + """ + maxUpdates: Int +} + +type DownloadEdge implements Edge { + cursor: Cursor! + + node: DownloadType! +} + +type DownloadNodeList implements NodeList { + edges: [DownloadEdge!]! + + nodes: [DownloadType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum DownloadState { + QUEUED + + DOWNLOADING + + FINISHED + + ERROR +} + +type DownloadStatus { + queue: [DownloadType!]! + + state: DownloaderState! +} + +type DownloadType { + position: Int! + + progress: Float! + + state: DownloadState! + + tries: Int! + + chapter: ChapterType! + + manga: MangaType! +} + +type DownloadUpdate { + download: DownloadType! + + type: DownloadUpdateType! +} + +enum DownloadUpdateType { + QUEUED + + DEQUEUED + + PAUSED + + STOPPED + + PROGRESS + + FINISHED + + ERROR + + POSITION +} + +type DownloadUpdates { + """ + The current download queue at the time of sending initial message. Is null for all following messages + """ + initial: [DownloadType!] + + """ + Indicates whether updates have been omitted based on the "maxUpdates" subscription variable. In case updates have been omitted, the "downloadStatus" query should be re-fetched. + """ + omittedUpdates: Boolean! + + state: DownloaderState! + + updates: [DownloadUpdate!]! +} + +enum DownloaderState { + STARTED + + STOPPED +} + +""" +An ISO-8601 encoded duration string +""" +scalar Duration + +interface Edge { + """ + A cursor for use in pagination. + """ + cursor: Cursor! + + """ + The [T] at the end of the edge. + """ + node: Node! +} + +type EditTextPreference { + currentValue: String + + default: String + + dialogMessage: String + + dialogTitle: String + + enabled: Boolean! + + key: String + + summary: String + + text: String + + title: String + + visible: Boolean! +} + +input EnqueueChapterDownloadInput { + clientMutationId: String + + id: Int! +} + +type EnqueueChapterDownloadPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input EnqueueChapterDownloadsInput { + clientMutationId: String + + ids: [Int!]! +} + +type EnqueueChapterDownloadsPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input ExtensionConditionInput { + apkName: String + + apkUrl: String + + contentWarning: ContentWarning + + extensionLib: String + + hasUpdate: Boolean + + iconUrl: String + + isInstalled: Boolean + + isNsfw: Boolean @deprecated(reason: ", replace with contentWarning") + + isObsolete: Boolean + + jarUrl: String + + lang: String + + name: String + + pkgName: String + + repo: String @deprecated(reason: ", replace with storeIndexUrl") + + storeIndexUrl: String + + versionCode: Int + + versionCodeLong: LongString + + versionName: String +} + +type ExtensionEdge implements Edge { + cursor: Cursor! + + node: ExtensionType! +} + +input ExtensionFilterInput { + and: [ExtensionFilterInput!] + + apkName: StringFilterInput + + apkUrl: StringFilterInput + + contentWarning: ContentWarningFilterInput + + extensionLib: StringFilterInput + + hasUpdate: BooleanFilterInput + + iconUrl: StringFilterInput + + isInstalled: BooleanFilterInput + + isNsfw: BooleanFilterInput @deprecated(reason: ", replace with contentWarning") + + isObsolete: BooleanFilterInput + + jarUrl: StringFilterInput + + lang: StringFilterInput + + name: StringFilterInput + + not: ExtensionFilterInput + + or: [ExtensionFilterInput!] + + pkgName: StringFilterInput + + repo: StringFilterInput @deprecated(reason: ", replace with storeIndexUrl") + + storeIndexUrl: StringFilterInput + + versionCode: IntFilterInput @deprecated(reason: ", replace with versionCodeLong") + + versionCodeLong: LongFilterInput + + versionName: StringFilterInput +} + +type ExtensionNodeList implements NodeList { + edges: [ExtensionEdge!]! + + nodes: [ExtensionType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum ExtensionOrderBy { + PKG_NAME + + NAME + + APK_NAME @deprecated(reason: "") +} + +input ExtensionOrderInput { + by: ExtensionOrderBy! + + byType: SortOrder +} + +input ExtensionStoreConditionInput { + id: Int + + indexUrl: String + + name: String +} + +type ExtensionStoreEdge implements Edge { + cursor: Cursor! + + node: ExtensionStoreType! +} + +input ExtensionStoreFilterInput { + and: [ExtensionStoreFilterInput!] + + indexUrl: StringFilterInput + + name: StringFilterInput + + not: ExtensionStoreFilterInput + + or: [ExtensionStoreFilterInput!] +} + +type ExtensionStoreNodeList implements NodeList { + edges: [ExtensionStoreEdge!]! + + nodes: [ExtensionStoreType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum ExtensionStoreOrderBy { + NAME + + INDEX_URL +} + +input ExtensionStoreOrderInput { + by: ExtensionStoreOrderBy! + + byType: SortOrder +} + +type ExtensionStoreType { + badgeLabel: String! + + contactDiscord: String + + contactWebsite: String! + + extensionListUrl: String + + indexUrl: String! + + isLegacy: Boolean! + + name: String! + + signingKey: String! + + extensions: ExtensionNodeList! +} + +type ExtensionType { + """ + This will be nullable in the future + """ + apkName: String + + apkUrl: String + + contentWarning: ContentWarning! + + extensionLib: String + + hasUpdate: Boolean! + + iconUrl: String! + + isInstalled: Boolean! + + isNsfw: Boolean! @deprecated(reason: "Removed in extension api v1.6, replace with contentWarning") + + isObsolete: Boolean! + + jarUrl: String + + lang: String! + + name: String! + + pkgName: String! + + repo: String @deprecated(reason: "Removed in extension api v1.6, replace with storeIndexUrl") + + storeIndexUrl: String + + versionCode: Int! @deprecated(reason: "Type was changed to Long, will be switched back to this variable name in the future., replace with versionCodeLong") + + versionCodeLong: LongString! + + versionName: String! + + extensionStore: ExtensionStoreType + + source: SourceNodeList! +} + +input FetchChapterPagesInput { + chapterId: Int! + + clientMutationId: String + + format: String +} + +type FetchChapterPagesPayload { + chapter: ChapterType! + + clientMutationId: String + + pages: [String!]! + + syncConflict: SyncConflictInfoType +} + +input FetchChaptersInput { + clientMutationId: String + + mangaId: Int! +} + +type FetchChaptersPayload { + chapters: [ChapterType!]! + + clientMutationId: String +} + +input FetchExtensionsInput { + clientMutationId: String +} + +type FetchExtensionsPayload { + clientMutationId: String + + extensionStores: [ExtensionStoreType!]! + + extensions: [ExtensionType!]! +} + +input FetchMangaAndChaptersInput { + clientMutationId: String + + fetchChapters: Boolean! + + fetchManga: Boolean! + + id: Int! +} + +type FetchMangaAndChaptersPayload { + chapters: [ChapterType!]! + + clientMutationId: String + + manga: MangaType! +} + +input FetchMangaInput { + clientMutationId: String + + id: Int! +} + +type FetchMangaPayload { + clientMutationId: String + + manga: MangaType! +} + +input FetchSourceMangaInput { + clientMutationId: String + + filters: [FilterChangeInput!] + + page: Int! + + query: String + + source: LongString! + + type: FetchSourceMangaType! +} + +type FetchSourceMangaPayload { + clientMutationId: String + + hasNextPage: Boolean! + + mangas: [MangaType!]! +} + +enum FetchSourceMangaType { + SEARCH + + POPULAR + + LATEST +} + +input FetchTrackInput { + clientMutationId: String + + recordId: Int! +} + +type FetchTrackPayload { + clientMutationId: String + + trackRecord: TrackRecordType! +} + +union Filter = CheckBoxFilter|GroupFilter|HeaderFilter|SelectFilter|SeparatorFilter|SortFilter|TextFilter|TriStateFilter + +input FilterChangeInput { + checkBoxState: Boolean + + groupChange: FilterChangeInput + + position: Int! + + selectState: Int + + sortState: SortSelectionInput + + textState: String + + triState: TriState +} + +""" +Built-in Float +""" +scalar Float + +type GlobalMetaNodeList implements NodeList { + edges: [MetaEdge!]! + + nodes: [GlobalMetaType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +type GlobalMetaType implements MetaType { + key: String! + + value: String! +} + +input GlobalMetaTypeInput { + key: String! + + value: String! +} + +type GroupFilter { + filters: [Filter!]! + + name: String! +} + +type HeaderFilter { + name: String! +} + +enum IncludeOrExclude { + EXCLUDE + + INCLUDE + + UNSET +} + +input InstallExternalExtensionInput { + clientMutationId: String + + extensionFile: Upload! +} + +type InstallExternalExtensionPayload { + clientMutationId: String + + extension: ExtensionType! +} + +""" +Built-in Int +""" +scalar Int + +input IntFilterInput { + distinctFrom: Int + + distinctFromAll: [Int!] + + distinctFromAny: [Int!] + + equalTo: Int + + greaterThan: Int + + greaterThanOrEqualTo: Int + + in: [Int!] + + isNull: Boolean + + lessThan: Int + + lessThanOrEqualTo: Int + + notDistinctFrom: Int + + notEqualTo: Int + + notEqualToAll: [Int!] + + notEqualToAny: [Int!] + + notIn: [Int!] +} + +type KoSyncConnectPayload { + clientMutationId: String + + message: String + + status: KoSyncStatusPayload! +} + +type KoSyncStatusPayload { + isLoggedIn: Boolean! + + serverAddress: String + + username: String +} + +enum KoreaderSyncChecksumMethod { + BINARY + + FILENAME +} + +enum KoreaderSyncConflictStrategy { + PROMPT + + KEEP_LOCAL + + KEEP_REMOTE + + DISABLED +} + +enum KoreaderSyncLegacyStrategy { + PROMPT + + SILENT + + SEND + + RECEIVE + + DISABLED +} + +type LastUpdateTimestampPayload { + timestamp: LongString! +} + +type LibraryUpdateStatus { + categoryUpdates: [CategoryUpdateType!]! + + jobsInfo: UpdaterJobsInfoType! + + mangaUpdates: [MangaUpdateType!]! +} + +input LibraryUpdateStatusChangedInput { + """ + Sets a max number of updates that can be contained in a updater update message.Everything above this limit will be omitted and the "updateStatus" should be re-fetched via the corresponding query. Due to the graphql subscription execution strategy not supporting batching for data loaders, the data loaders run into the n+1 problem, which can cause the server to get unresponsive until the status update has been handled. This is an issue e.g. when starting an update. + """ + maxUpdates: Int +} + +type ListPreference { + currentValue: String + + default: String + + enabled: Boolean! + + entries: [String!]! + + entryValues: [String!]! + + key: String + + summary: String + + title: String + + visible: Boolean! +} + +input LoginInput { + clientMutationId: String + + password: String! + + username: String! +} + +type LoginPayload { + accessToken: String! + + clientMutationId: String + + refreshToken: String! +} + +input LoginTrackerCredentialsInput { + clientMutationId: String + + password: String! + + trackerId: Int! + + username: String! +} + +type LoginTrackerCredentialsPayload { + clientMutationId: String + + isLoggedIn: Boolean! + + tracker: TrackerType! +} + +input LoginTrackerOAuthInput { + callbackUrl: String! + + clientMutationId: String + + trackerId: Int! +} + +type LoginTrackerOAuthPayload { + clientMutationId: String + + isLoggedIn: Boolean! + + tracker: TrackerType! +} + +input LogoutKoSyncAccountInput { + clientMutationId: String +} + +type LogoutKoSyncAccountPayload { + clientMutationId: String + + status: KoSyncStatusPayload! +} + +input LogoutTrackerInput { + clientMutationId: String + + trackerId: Int! +} + +type LogoutTrackerPayload { + clientMutationId: String + + isLoggedIn: Boolean! + + tracker: TrackerType! +} + +input LongFilterInput { + distinctFrom: LongString + + distinctFromAll: [LongString!] + + distinctFromAny: [LongString!] + + equalTo: LongString + + greaterThan: LongString + + greaterThanOrEqualTo: LongString + + in: [LongString!] + + isNull: Boolean + + lessThan: LongString + + lessThanOrEqualTo: LongString + + notDistinctFrom: LongString + + notEqualTo: LongString + + notEqualToAll: [LongString!] + + notEqualToAny: [LongString!] + + notIn: [LongString!] +} + +""" +A 64-bit signed integer as a String +""" +scalar LongString + +input MangaConditionInput { + artist: String + + author: String + + categoryIds: [Int!] + + chaptersLastFetchedAt: LongString + + description: String + + genre: [String!] + + id: Int + + inLibrary: Boolean + + inLibraryAt: LongString + + initialized: Boolean + + lastFetchedAt: LongString + + realUrl: String + + sourceId: LongString + + status: MangaStatus + + thumbnailUrl: String + + title: String + + url: String +} + +type MangaEdge implements Edge { + cursor: Cursor! + + node: MangaType! +} + +input MangaFilterInput { + and: [MangaFilterInput!] + + artist: StringFilterInput + + author: StringFilterInput + + categoryId: IntFilterInput + + chaptersLastFetchedAt: LongFilterInput + + description: StringFilterInput + + genre: StringFilterInput + + id: IntFilterInput + + inLibrary: BooleanFilterInput + + inLibraryAt: LongFilterInput + + initialized: BooleanFilterInput + + lastFetchedAt: LongFilterInput + + not: MangaFilterInput + + or: [MangaFilterInput!] + + realUrl: StringFilterInput + + sourceId: LongFilterInput + + status: MangaStatusFilterInput + + thumbnailUrl: StringFilterInput + + title: StringFilterInput + + url: StringFilterInput +} + +enum MangaJobStatus { + PENDING + + RUNNING + + COMPLETE + + FAILED + + SKIPPED +} + +type MangaMetaType implements MetaType { + key: String! + + mangaId: Int! + + value: String! + + manga: MangaType! +} + +input MangaMetaTypeInput { + key: String! + + mangaId: Int! + + value: String! +} + +type MangaNodeList implements NodeList { + edges: [MangaEdge!]! + + nodes: [MangaType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum MangaOrderBy { + ID + + TITLE + + IN_LIBRARY_AT + + LAST_FETCHED_AT +} + +input MangaOrderInput { + by: MangaOrderBy! + + byType: SortOrder +} + +enum MangaStatus { + UNKNOWN + + ONGOING + + COMPLETED + + LICENSED + + PUBLISHING_FINISHED + + CANCELLED + + ON_HIATUS +} + +input MangaStatusFilterInput { + distinctFrom: MangaStatus + + distinctFromAll: [MangaStatus!] + + distinctFromAny: [MangaStatus!] + + equalTo: MangaStatus + + greaterThan: MangaStatus + + greaterThanOrEqualTo: MangaStatus + + in: [MangaStatus!] + + isNull: Boolean + + lessThan: MangaStatus + + lessThanOrEqualTo: MangaStatus + + notDistinctFrom: MangaStatus + + notEqualTo: MangaStatus + + notEqualToAll: [MangaStatus!] + + notEqualToAny: [MangaStatus!] + + notIn: [MangaStatus!] +} + +type MangaType { + artist: String + + author: String + + chaptersLastFetchedAt: LongString + + description: String + + genre: [String!]! + + id: Int! + + inLibrary: Boolean! + + inLibraryAt: LongString! + + initialized: Boolean! + + lastFetchedAt: LongString + + realUrl: String + + sourceId: LongString! + + status: MangaStatus! + + thumbnailUrl: String + + thumbnailUrlLastFetched: LongString + + title: String! + + updateStrategy: UpdateStrategy! + + url: String! + + age: LongString + + bookmarkCount: Int! + + categories: CategoryNodeList! + + chapters: ChapterNodeList! + + chaptersAge: LongString + + downloadCount: Int! + + firstUnreadChapter: ChapterType + + hasDuplicateChapters: Boolean! + + highestNumberedChapter: ChapterType + + lastReadChapter: ChapterType + + latestFetchedChapter: ChapterType + + latestReadChapter: ChapterType + + latestUploadedChapter: ChapterType + + meta: [MangaMetaType!]! + + source: SourceType + + trackRecords: TrackRecordNodeList! + + unreadCount: Int! +} + +type MangaUpdateType { + status: MangaJobStatus! + + manga: MangaType! +} + +input MetaConditionInput { + key: String + + value: String +} + +type MetaEdge implements Edge { + cursor: Cursor! + + node: GlobalMetaType! +} + +input MetaFilterInput { + and: [MetaFilterInput!] + + key: StringFilterInput + + not: MetaFilterInput + + or: [MetaFilterInput!] + + value: StringFilterInput +} + +input MetaInput { + key: String! + + value: String! +} + +enum MetaOrderBy { + KEY + + VALUE +} + +input MetaOrderInput { + by: MetaOrderBy! + + byType: SortOrder +} + +interface MetaType { + key: String! + + value: String! +} + +type MultiSelectListPreference { + currentValue: [String!] + + default: [String!] + + dialogMessage: String + + dialogTitle: String + + enabled: Boolean! + + entries: [String!]! + + entryValues: [String!]! + + key: String + + summary: String + + title: String + + visible: Boolean! +} + +type Mutation { + createBackup(input: CreateBackupInput): CreateBackupPayload! + + restoreBackup(input: RestoreBackupInput!): RestoreBackupPayload! + + createCategory(input: CreateCategoryInput!): CreateCategoryPayload + + deleteCategory(input: DeleteCategoryInput!): DeleteCategoryPayload + + deleteCategoryMeta(input: DeleteCategoryMetaInput!): DeleteCategoryMetaPayload + + deleteCategoryMetas(input: DeleteCategoryMetasInput!): DeleteCategoryMetasPayload + + setCategoryMeta(input: SetCategoryMetaInput!): SetCategoryMetaPayload + + setCategoryMetas(input: SetCategoryMetasInput!): SetCategoryMetasPayload + + updateCategories(input: UpdateCategoriesInput!): UpdateCategoriesPayload + + updateCategory(input: UpdateCategoryInput!): UpdateCategoryPayload + + updateCategoryOrder(input: UpdateCategoryOrderInput!): UpdateCategoryOrderPayload + + updateMangaCategories(input: UpdateMangaCategoriesInput!): UpdateMangaCategoriesPayload + + updateMangasCategories(input: UpdateMangasCategoriesInput!): UpdateMangasCategoriesPayload + + deleteChapterMeta(input: DeleteChapterMetaInput!): DeleteChapterMetaPayload + + deleteChapterMetas(input: DeleteChapterMetasInput!): DeleteChapterMetasPayload + + fetchChapterPages(input: FetchChapterPagesInput!): FetchChapterPagesPayload + + fetchChapters(input: FetchChaptersInput!): FetchChaptersPayload @deprecated(reason: "Deprecated in Tachiyomix 1.6, replace with fetchMangaAndChapters") + + setChapterMeta(input: SetChapterMetaInput!): SetChapterMetaPayload + + setChapterMetas(input: SetChapterMetasInput!): SetChapterMetasPayload + + updateChapter(input: UpdateChapterInput!): UpdateChapterPayload + + updateChapters(input: UpdateChaptersInput!): UpdateChaptersPayload + + clearDownloader(input: ClearDownloaderInput!): ClearDownloaderPayload + + deleteDownloadedChapter(input: DeleteDownloadedChapterInput!): DeleteDownloadedChapterPayload + + deleteDownloadedChapters(input: DeleteDownloadedChaptersInput!): DeleteDownloadedChaptersPayload + + dequeueChapterDownload(input: DequeueChapterDownloadInput!): DequeueChapterDownloadPayload + + dequeueChapterDownloads(input: DequeueChapterDownloadsInput!): DequeueChapterDownloadsPayload + + enqueueChapterDownload(input: EnqueueChapterDownloadInput!): EnqueueChapterDownloadPayload + + enqueueChapterDownloads(input: EnqueueChapterDownloadsInput!): EnqueueChapterDownloadsPayload + + reorderChapterDownload(input: ReorderChapterDownloadInput!): ReorderChapterDownloadPayload + + startDownloader(input: StartDownloaderInput!): StartDownloaderPayload + + stopDownloader(input: StopDownloaderInput!): StopDownloaderPayload + + fetchExtensions(input: FetchExtensionsInput!): FetchExtensionsPayload + + installExternalExtension(input: InstallExternalExtensionInput!): InstallExternalExtensionPayload + + updateExtension(input: UpdateExtensionInput!): UpdateExtensionPayload + + updateExtensions(input: UpdateExtensionsInput!): UpdateExtensionsPayload + + addExtensionStore(input: AddExtensionStoreInput!): AddExtensionStorePayload + + removeExtensionStore(input: RemoveExtensionStoreInput!): RemoveExtensionStorePayload + + clearCachedImages(input: ClearCachedImagesInput!): ClearCachedImagesPayload! + + resetWebUIUpdateStatus: WebUIUpdateStatus + + updateWebUI(input: WebUIUpdateInput!): WebUIUpdatePayload + + connectKoSyncAccount(input: ConnectKoSyncAccountInput!): KoSyncConnectPayload! + + logoutKoSyncAccount(input: LogoutKoSyncAccountInput!): LogoutKoSyncAccountPayload! + + pullKoSyncProgress(input: PullKoSyncProgressInput!): PullKoSyncProgressPayload + + pushKoSyncProgress(input: PushKoSyncProgressInput!): PushKoSyncProgressPayload + + deleteMangaMeta(input: DeleteMangaMetaInput!): DeleteMangaMetaPayload + + deleteMangaMetas(input: DeleteMangaMetasInput!): DeleteMangaMetasPayload + + fetchManga(input: FetchMangaInput!): FetchMangaPayload @deprecated(reason: "Deprecated in Tachiyomix 1.6, replace with fetchMangaAndChapters") + + fetchMangaAndChapters(input: FetchMangaAndChaptersInput!): FetchMangaAndChaptersPayload + + setMangaMeta(input: SetMangaMetaInput!): SetMangaMetaPayload + + setMangaMetas(input: SetMangaMetasInput!): SetMangaMetasPayload + + updateManga(input: UpdateMangaInput!): UpdateMangaPayload + + updateMangas(input: UpdateMangasInput!): UpdateMangasPayload + + deleteGlobalMeta(input: DeleteGlobalMetaInput!): DeleteGlobalMetaPayload + + deleteGlobalMetas(input: DeleteGlobalMetasInput!): DeleteGlobalMetasPayload + + setGlobalMeta(input: SetGlobalMetaInput!): SetGlobalMetaPayload + + setGlobalMetas(input: SetGlobalMetasInput!): SetGlobalMetasPayload + + resetSettings(input: ResetSettingsInput!): ResetSettingsPayload! + + setSettings(input: SetSettingsInput!): SetSettingsPayload! + + startSync(input: StartSyncInput!): StartSyncPayload! + + deleteSourceMeta(input: DeleteSourceMetaInput!): DeleteSourceMetaPayload + + deleteSourceMetas(input: DeleteSourceMetasInput!): DeleteSourceMetasPayload + + fetchSourceManga(input: FetchSourceMangaInput!): FetchSourceMangaPayload + + setSourceMeta(input: SetSourceMetaInput!): SetSourceMetaPayload + + setSourceMetas(input: SetSourceMetasInput!): SetSourceMetasPayload + + updateSourcePreference(input: UpdateSourcePreferenceInput!): UpdateSourcePreferencePayload + + bindTrack(input: BindTrackInput!): BindTrackPayload! + + bindTrackRecord(input: BindTrackRecordInput!): BindTrackRecordPayload + + fetchTrack(input: FetchTrackInput!): FetchTrackPayload! + + loginTrackerCredentials(input: LoginTrackerCredentialsInput!): LoginTrackerCredentialsPayload! + + loginTrackerOAuth(input: LoginTrackerOAuthInput!): LoginTrackerOAuthPayload! + + logoutTracker(input: LogoutTrackerInput!): LogoutTrackerPayload! + + trackProgress(input: TrackProgressInput!): TrackProgressPayload + + unbindTrack(input: UnbindTrackInput!): UnbindTrackPayload! + + updateTrack(input: UpdateTrackInput!): UpdateTrackPayload! + + updateCategoryManga(input: UpdateCategoryMangaInput!): UpdateCategoryMangaPayload + + updateLibrary(input: UpdateLibraryInput!): UpdateLibraryPayload + + updateLibraryManga(input: UpdateLibraryMangaInput!): UpdateLibraryMangaPayload + + updateStop(input: UpdateStopInput!): UpdateStopPayload! + + login(input: LoginInput!): LoginPayload! + + refreshToken(input: RefreshTokenInput!): RefreshTokenPayload! +} + +union Node = CategoryMetaType|CategoryType|ChapterMetaType|ChapterType|DownloadType|DownloadUpdate|ExtensionStoreType|ExtensionType|GlobalMetaType|MangaMetaType|MangaType|PartialSettingsType|SettingsType|SourceMetaType|SourceType|TrackRecordType|TrackerType + +interface NodeList { + """ + A list of edges which contains the [T] and cursor to aid in pagination. + """ + edges: [Edge!]! + + """ + A list of [T] objects. + """ + nodes: [Node!]! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + The count of all nodes you could get from the connection. + """ + totalCount: Int! +} + +type PageInfo { + """ + When paginating forwards, the cursor to continue. + """ + endCursor: Cursor + + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: Cursor +} + +input PartialBackupFlagsInput { + includeCategories: Boolean + + includeChapters: Boolean + + includeClientData: Boolean + + includeHistory: Boolean + + includeManga: Boolean + + includeServerSettings: Boolean + + includeTracking: Boolean +} + +type PartialSettingsType implements Settings { + authMode: AuthMode + + authPassword: String + + authUsername: String + + autoBackupIncludeCategories: Boolean + + autoBackupIncludeChapters: Boolean + + autoBackupIncludeClientData: Boolean + + autoBackupIncludeHistory: Boolean + + autoBackupIncludeManga: Boolean + + autoBackupIncludeServerSettings: Boolean + + autoBackupIncludeTracking: Boolean + + autoDownloadAheadLimit: Int @deprecated(reason: "Replaced with autoDownloadNewChaptersLimit, replace with autoDownloadNewChaptersLimit") + + autoDownloadIgnoreReUploads: Boolean + + autoDownloadNewChapters: Boolean + + autoDownloadNewChaptersLimit: Int + + backupInterval: Int + + backupPath: String + + backupTTL: Int + + backupTime: String + + basicAuthEnabled: Boolean @deprecated(reason: "Removed - prefer authMode, replace with authMode") + + basicAuthPassword: String @deprecated(reason: "Removed - prefer authPassword, replace with authPassword") + + basicAuthUsername: String @deprecated(reason: "Removed - prefer authUsername, replace with authUsername") + + databasePassword: String + + databaseType: DatabaseType + + databaseUrl: String + + databaseUsername: String + + debugLogsEnabled: Boolean + + downloadAsCbz: Boolean + + downloadConversions: [SettingsDownloadConversionType!] + + downloadsPath: String + + electronPath: String + + excludeCompleted: Boolean + + excludeEntryWithUnreadChapters: Boolean + + excludeNotStarted: Boolean + + excludeUnreadChapters: Boolean + + extensionRepos: [String!] @deprecated(reason: "Replaced with addExtensionStore and removeExtensionStore mutations") + + flareSolverrAsResponseFallback: Boolean + + flareSolverrEnabled: Boolean + + flareSolverrSessionName: String + + flareSolverrSessionTtl: Int + + flareSolverrTimeout: Int + + flareSolverrUrl: String + + globalUpdateInterval: Float + + gqlDebugLogsEnabled: Boolean @deprecated(reason: "Removed - does not do anything") + + initialOpenInBrowserEnabled: Boolean + + ip: String + + jwtAudience: String + + jwtRefreshExpiry: Duration + + jwtTokenExpiry: Duration + + kcefEnabled: Boolean + + koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod + + koreaderSyncDeviceId: String @deprecated(reason: "Moved to preference store. Is supposed to be random and gets auto generated, replace with MOVE TO PREFERENCES") + + koreaderSyncPercentageTolerance: Float + + koreaderSyncServerUrl: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncStrategy: KoreaderSyncLegacyStrategy @deprecated(reason: "Replaced with koreaderSyncStrategyForward and koreaderSyncStrategyBackward, replace with koreaderSyncStrategyForward, koreaderSyncStrategyBackward") + + koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy + + koreaderSyncStrategyForward: KoreaderSyncConflictStrategy + + koreaderSyncUserkey: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncUsername: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + localSourcePath: String + + maxLogFileSize: String + + maxLogFiles: Int + + maxLogFolderSize: String + + maxSourcesInParallel: Int + + opdsCbzMimetype: CbzMediaType + + opdsChapterSortOrder: SortOrder + + opdsEnablePageReadProgress: Boolean + + opdsItemsPerPage: Int + + opdsMarkAsReadOnDownload: Boolean + + opdsShowOnlyDownloadedChapters: Boolean + + opdsShowOnlyUnreadChapters: Boolean + + opdsSkipChapterMetadataFeed: Boolean + + opdsUseBinaryFileSizes: Boolean + + port: Int + + serveConversions: [SettingsDownloadConversionType!] + + socksProxyEnabled: Boolean + + socksProxyHost: String + + socksProxyPassword: String + + socksProxyPort: String + + socksProxyUsername: String + + socksProxyVersion: Int + + syncDataCategories: Boolean + + syncDataChapters: Boolean + + syncDataHistory: Boolean + + syncDataManga: Boolean + + syncDataTracking: Boolean + + syncInterval: Duration + + syncYomiApiKey: String + + syncYomiEnabled: Boolean + + syncYomiHost: String + + systemTrayEnabled: Boolean + + updateMangas: Boolean + + useHikariConnectionPool: Boolean + + webUIChannel: WebUIChannel + + webUIFlavor: WebUIFlavor + + webUIInterface: WebUIInterface + + webUIUpdateCheckInterval: Float +} + +input PartialSettingsTypeInput { + authMode: AuthMode + + authPassword: String + + authUsername: String + + autoBackupIncludeCategories: Boolean + + autoBackupIncludeChapters: Boolean + + autoBackupIncludeClientData: Boolean + + autoBackupIncludeHistory: Boolean + + autoBackupIncludeManga: Boolean + + autoBackupIncludeServerSettings: Boolean + + autoBackupIncludeTracking: Boolean + + autoDownloadAheadLimit: Int @deprecated(reason: "Replaced with autoDownloadNewChaptersLimit, replace with autoDownloadNewChaptersLimit") + + autoDownloadIgnoreReUploads: Boolean + + autoDownloadNewChapters: Boolean + + autoDownloadNewChaptersLimit: Int + + backupInterval: Int + + backupPath: String + + backupTTL: Int + + backupTime: String + + basicAuthEnabled: Boolean @deprecated(reason: "Removed - prefer authMode, replace with authMode") + + basicAuthPassword: String @deprecated(reason: "Removed - prefer authPassword, replace with authPassword") + + basicAuthUsername: String @deprecated(reason: "Removed - prefer authUsername, replace with authUsername") + + databasePassword: String + + databaseType: DatabaseType + + databaseUrl: String + + databaseUsername: String + + debugLogsEnabled: Boolean + + downloadAsCbz: Boolean + + downloadConversions: [SettingsDownloadConversionTypeInput!] + + downloadsPath: String + + electronPath: String + + excludeCompleted: Boolean + + excludeEntryWithUnreadChapters: Boolean + + excludeNotStarted: Boolean + + excludeUnreadChapters: Boolean + + extensionRepos: [String!] @deprecated(reason: "Replaced with addExtensionStore and removeExtensionStore mutations") + + flareSolverrAsResponseFallback: Boolean + + flareSolverrEnabled: Boolean + + flareSolverrSessionName: String + + flareSolverrSessionTtl: Int + + flareSolverrTimeout: Int + + flareSolverrUrl: String + + globalUpdateInterval: Float + + gqlDebugLogsEnabled: Boolean @deprecated(reason: "Removed - does not do anything") + + initialOpenInBrowserEnabled: Boolean + + ip: String + + jwtAudience: String + + jwtRefreshExpiry: Duration + + jwtTokenExpiry: Duration + + kcefEnabled: Boolean + + koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod + + koreaderSyncDeviceId: String @deprecated(reason: "Moved to preference store. Is supposed to be random and gets auto generated, replace with MOVE TO PREFERENCES") + + koreaderSyncPercentageTolerance: Float + + koreaderSyncServerUrl: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncStrategy: KoreaderSyncLegacyStrategy @deprecated(reason: "Replaced with koreaderSyncStrategyForward and koreaderSyncStrategyBackward, replace with koreaderSyncStrategyForward, koreaderSyncStrategyBackward") + + koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy + + koreaderSyncStrategyForward: KoreaderSyncConflictStrategy + + koreaderSyncUserkey: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncUsername: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + localSourcePath: String + + maxLogFileSize: String + + maxLogFiles: Int + + maxLogFolderSize: String + + maxSourcesInParallel: Int + + opdsCbzMimetype: CbzMediaType + + opdsChapterSortOrder: SortOrder + + opdsEnablePageReadProgress: Boolean + + opdsItemsPerPage: Int + + opdsMarkAsReadOnDownload: Boolean + + opdsShowOnlyDownloadedChapters: Boolean + + opdsShowOnlyUnreadChapters: Boolean + + opdsSkipChapterMetadataFeed: Boolean + + opdsUseBinaryFileSizes: Boolean + + port: Int + + serveConversions: [SettingsDownloadConversionTypeInput!] + + socksProxyEnabled: Boolean + + socksProxyHost: String + + socksProxyPassword: String + + socksProxyPort: String + + socksProxyUsername: String + + socksProxyVersion: Int + + syncDataCategories: Boolean + + syncDataChapters: Boolean + + syncDataHistory: Boolean + + syncDataManga: Boolean + + syncDataTracking: Boolean + + syncInterval: Duration + + syncYomiApiKey: String + + syncYomiEnabled: Boolean + + syncYomiHost: String + + systemTrayEnabled: Boolean + + updateMangas: Boolean + + useHikariConnectionPool: Boolean + + webUIChannel: WebUIChannel + + webUIFlavor: WebUIFlavor + + webUIInterface: WebUIInterface + + webUIUpdateCheckInterval: Float +} + +union Preference = CheckBoxPreference|EditTextPreference|ListPreference|MultiSelectListPreference|SwitchPreference + +input PullKoSyncProgressInput { + chapterId: Int! + + clientMutationId: String +} + +type PullKoSyncProgressPayload { + chapter: ChapterType + + clientMutationId: String + + syncConflict: SyncConflictInfoType +} + +input PushKoSyncProgressInput { + chapterId: Int! + + clientMutationId: String +} + +type PushKoSyncProgressPayload { + chapter: ChapterType + + clientMutationId: String + + success: Boolean! +} + +type Query { + restoreStatus(id: String!): BackupRestoreStatus + + validateBackup(input: ValidateBackupInput!): ValidateBackupResult! + + categories(condition: CategoryConditionInput, filter: CategoryFilterInput, orderBy: CategoryOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [CategoryOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): CategoryNodeList! + + category(id: Int!): CategoryType! + + chapter(id: Int!): ChapterType! + + chapters(condition: ChapterConditionInput, filter: ChapterFilterInput, orderBy: ChapterOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [ChapterOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): ChapterNodeList! + + downloadStatus: DownloadStatus! + + extension(pkgName: String!): ExtensionType! + + extensions(condition: ExtensionConditionInput, filter: ExtensionFilterInput, orderBy: ExtensionOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [ExtensionOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): ExtensionNodeList! + + extensionStore(indexUrl: String!): ExtensionStoreType! + + extensionStores(condition: ExtensionStoreConditionInput, filter: ExtensionStoreFilterInput, order: [ExtensionStoreOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): ExtensionStoreNodeList! + + aboutServer: AboutServerPayload! + + aboutWebUI: AboutWebUI! + + checkForServerUpdates: [CheckForServerUpdatesPayload!]! + + checkForWebUIUpdate: WebUIUpdateCheck! + + getWebUIUpdateStatus: WebUIUpdateStatus! + + koSyncStatus: KoSyncStatusPayload! + + manga(id: Int!): MangaType! + + mangas(condition: MangaConditionInput, filter: MangaFilterInput, orderBy: MangaOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [MangaOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): MangaNodeList! + + meta(key: String!): GlobalMetaType! + + metas(condition: MetaConditionInput, filter: MetaFilterInput, orderBy: MetaOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [MetaOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): GlobalMetaNodeList! + + settings: SettingsType! + + source(id: LongString!): SourceType! + + sources(condition: SourceConditionInput, filter: SourceFilterInput, orderBy: SourceOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [SourceOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): SourceNodeList! + + lastSyncStatus: SyncStatus + + searchTracker(input: SearchTrackerInput!): SearchTrackerPayload! + + trackRecord(id: Int!): TrackRecordType! + + trackRecords(condition: TrackRecordConditionInput, filter: TrackRecordFilterInput, orderBy: TrackRecordOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [TrackRecordOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): TrackRecordNodeList! + + tracker(id: Int!): TrackerType! + + trackers(condition: TrackerConditionInput, orderBy: TrackerOrderBy @deprecated(reason: "Replaced with order, replace with order"), orderByType: SortOrder @deprecated(reason: "Replaced with order, replace with order"), order: [TrackerOrderInput!], before: Cursor, after: Cursor, first: Int, last: Int, offset: Int): TrackerNodeList! + + lastUpdateTimestamp: LastUpdateTimestampPayload! + + libraryUpdateStatus: LibraryUpdateStatus! + + updateStatus: UpdateStatus! @deprecated(reason: "Replaced with libraryUpdateStatus, replace with libraryUpdateStatus") +} + +input RefreshTokenInput { + clientMutationId: String + + refreshToken: String! +} + +type RefreshTokenPayload { + accessToken: String! + + clientMutationId: String +} + +input RemoveExtensionStoreInput { + clientMutationId: String + + indexUrl: String! +} + +type RemoveExtensionStorePayload { + clientMutationId: String + + extensionStore: ExtensionStoreType +} + +input ReorderChapterDownloadInput { + chapterId: Int! + + clientMutationId: String + + to: Int! +} + +type ReorderChapterDownloadPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input ResetSettingsInput { + clientMutationId: String +} + +type ResetSettingsPayload { + clientMutationId: String + + settings: SettingsType! +} + +input RestoreBackupInput { + backup: Upload! + + clientMutationId: String + + flags: PartialBackupFlagsInput +} + +type RestoreBackupPayload { + clientMutationId: String + + id: String! + + status: BackupRestoreStatus +} + +input SearchTrackerInput { + query: String! + + trackerId: Int! +} + +type SearchTrackerPayload { + trackSearches: [TrackSearchType!]! +} + +type SelectFilter { + default: Int! + + name: String! + + values: [String!]! +} + +type SeparatorFilter { + name: String! +} + +input SetCategoryMetaInput { + clientMutationId: String + + meta: CategoryMetaTypeInput! +} + +type SetCategoryMetaPayload { + clientMutationId: String + + meta: CategoryMetaType! +} + +input SetCategoryMetasInput { + clientMutationId: String + + items: [SetCategoryMetasItemInput!]! +} + +input SetCategoryMetasItemInput { + categoryIds: [Int!]! + + metas: [MetaInput!]! +} + +type SetCategoryMetasPayload { + categories: [CategoryType!]! + + clientMutationId: String + + metas: [CategoryMetaType!]! +} + +input SetChapterMetaInput { + clientMutationId: String + + meta: ChapterMetaTypeInput! +} + +type SetChapterMetaPayload { + clientMutationId: String + + meta: ChapterMetaType! +} + +input SetChapterMetasInput { + clientMutationId: String + + items: [SetChapterMetasItemInput!]! +} + +input SetChapterMetasItemInput { + chapterIds: [Int!]! + + metas: [MetaInput!]! +} + +type SetChapterMetasPayload { + chapters: [ChapterType!]! + + clientMutationId: String + + metas: [ChapterMetaType!]! +} + +input SetGlobalMetaInput { + clientMutationId: String + + meta: GlobalMetaTypeInput! +} + +type SetGlobalMetaPayload { + clientMutationId: String + + meta: GlobalMetaType! +} + +input SetGlobalMetasInput { + clientMutationId: String + + metas: [MetaInput!]! +} + +type SetGlobalMetasPayload { + clientMutationId: String + + metas: [GlobalMetaType!]! +} + +input SetMangaMetaInput { + clientMutationId: String + + meta: MangaMetaTypeInput! +} + +type SetMangaMetaPayload { + clientMutationId: String + + meta: MangaMetaType! +} + +input SetMangaMetasInput { + clientMutationId: String + + items: [SetMangaMetasItemInput!]! +} + +input SetMangaMetasItemInput { + mangaIds: [Int!]! + + metas: [MetaInput!]! +} + +type SetMangaMetasPayload { + clientMutationId: String + + mangas: [MangaType!]! + + metas: [MangaMetaType!]! +} + +input SetSettingsInput { + clientMutationId: String + + settings: PartialSettingsTypeInput! +} + +type SetSettingsPayload { + clientMutationId: String + + settings: SettingsType! +} + +input SetSourceMetaInput { + clientMutationId: String + + meta: SourceMetaTypeInput! +} + +type SetSourceMetaPayload { + clientMutationId: String + + meta: SourceMetaType! +} + +input SetSourceMetasInput { + clientMutationId: String + + items: [SetSourceMetasItemInput!]! +} + +input SetSourceMetasItemInput { + metas: [MetaInput!]! + + sourceIds: [LongString!]! +} + +type SetSourceMetasPayload { + clientMutationId: String + + metas: [SourceMetaType!]! + + sources: [SourceType!]! +} + +interface Settings { + authMode: AuthMode + + authPassword: String + + authUsername: String + + autoBackupIncludeCategories: Boolean + + autoBackupIncludeChapters: Boolean + + autoBackupIncludeClientData: Boolean + + autoBackupIncludeHistory: Boolean + + autoBackupIncludeManga: Boolean + + autoBackupIncludeServerSettings: Boolean + + autoBackupIncludeTracking: Boolean + + autoDownloadAheadLimit: Int @deprecated(reason: "Replaced with autoDownloadNewChaptersLimit, replace with autoDownloadNewChaptersLimit") + + autoDownloadIgnoreReUploads: Boolean + + autoDownloadNewChapters: Boolean + + autoDownloadNewChaptersLimit: Int + + backupInterval: Int + + backupPath: String + + backupTTL: Int + + backupTime: String + + basicAuthEnabled: Boolean @deprecated(reason: "Removed - prefer authMode, replace with authMode") + + basicAuthPassword: String @deprecated(reason: "Removed - prefer authPassword, replace with authPassword") + + basicAuthUsername: String @deprecated(reason: "Removed - prefer authUsername, replace with authUsername") + + databasePassword: String + + databaseType: DatabaseType + + databaseUrl: String + + databaseUsername: String + + debugLogsEnabled: Boolean + + downloadAsCbz: Boolean + + downloadConversions: [SettingsDownloadConversion!] + + downloadsPath: String + + electronPath: String + + excludeCompleted: Boolean + + excludeEntryWithUnreadChapters: Boolean + + excludeNotStarted: Boolean + + excludeUnreadChapters: Boolean + + extensionRepos: [String!] @deprecated(reason: "Replaced with addExtensionStore and removeExtensionStore mutations") + + flareSolverrAsResponseFallback: Boolean + + flareSolverrEnabled: Boolean + + flareSolverrSessionName: String + + flareSolverrSessionTtl: Int + + flareSolverrTimeout: Int + + flareSolverrUrl: String + + globalUpdateInterval: Float + + gqlDebugLogsEnabled: Boolean @deprecated(reason: "Removed - does not do anything") + + initialOpenInBrowserEnabled: Boolean + + ip: String + + jwtAudience: String + + jwtRefreshExpiry: Duration + + jwtTokenExpiry: Duration + + kcefEnabled: Boolean + + koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod + + koreaderSyncDeviceId: String @deprecated(reason: "Moved to preference store. Is supposed to be random and gets auto generated, replace with MOVE TO PREFERENCES") + + koreaderSyncPercentageTolerance: Float + + koreaderSyncServerUrl: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncStrategy: KoreaderSyncLegacyStrategy @deprecated(reason: "Replaced with koreaderSyncStrategyForward and koreaderSyncStrategyBackward, replace with koreaderSyncStrategyForward, koreaderSyncStrategyBackward") + + koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy + + koreaderSyncStrategyForward: KoreaderSyncConflictStrategy + + koreaderSyncUserkey: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncUsername: String @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + localSourcePath: String + + maxLogFileSize: String + + maxLogFiles: Int + + maxLogFolderSize: String + + maxSourcesInParallel: Int + + opdsCbzMimetype: CbzMediaType + + opdsChapterSortOrder: SortOrder + + opdsEnablePageReadProgress: Boolean + + opdsItemsPerPage: Int + + opdsMarkAsReadOnDownload: Boolean + + opdsShowOnlyDownloadedChapters: Boolean + + opdsShowOnlyUnreadChapters: Boolean + + opdsSkipChapterMetadataFeed: Boolean + + opdsUseBinaryFileSizes: Boolean + + port: Int + + serveConversions: [SettingsDownloadConversion!] + + socksProxyEnabled: Boolean + + socksProxyHost: String + + socksProxyPassword: String + + socksProxyPort: String + + socksProxyUsername: String + + socksProxyVersion: Int + + syncDataCategories: Boolean + + syncDataChapters: Boolean + + syncDataHistory: Boolean + + syncDataManga: Boolean + + syncDataTracking: Boolean + + syncInterval: Duration + + syncYomiApiKey: String + + syncYomiEnabled: Boolean + + syncYomiHost: String + + systemTrayEnabled: Boolean + + updateMangas: Boolean + + useHikariConnectionPool: Boolean + + webUIChannel: WebUIChannel + + webUIFlavor: WebUIFlavor + + webUIInterface: WebUIInterface + + webUIUpdateCheckInterval: Float +} + +interface SettingsDownloadConversion { + callTimeout: Duration + + compressionLevel: Float + + connectTimeout: Duration + + headers: [SettingsDownloadConversionHeader!] + + mimeType: String! + + target: String! +} + +interface SettingsDownloadConversionHeader { + name: String! + + value: String! +} + +type SettingsDownloadConversionHeaderType implements SettingsDownloadConversionHeader { + name: String! + + value: String! +} + +input SettingsDownloadConversionHeaderTypeInput { + name: String! + + value: String! +} + +type SettingsDownloadConversionType implements SettingsDownloadConversion { + callTimeout: Duration + + compressionLevel: Float + + connectTimeout: Duration + + headers: [SettingsDownloadConversionHeaderType!] + + mimeType: String! + + target: String! +} + +input SettingsDownloadConversionTypeInput { + callTimeout: Duration + + compressionLevel: Float + + connectTimeout: Duration + + headers: [SettingsDownloadConversionHeaderTypeInput!] + + mimeType: String! + + target: String! +} + +type SettingsType implements Settings { + authMode: AuthMode! + + authPassword: String! + + authUsername: String! + + autoBackupIncludeCategories: Boolean! + + autoBackupIncludeChapters: Boolean! + + autoBackupIncludeClientData: Boolean! + + autoBackupIncludeHistory: Boolean! + + autoBackupIncludeManga: Boolean! + + autoBackupIncludeServerSettings: Boolean! + + autoBackupIncludeTracking: Boolean! + + autoDownloadAheadLimit: Int! @deprecated(reason: "Replaced with autoDownloadNewChaptersLimit, replace with autoDownloadNewChaptersLimit") + + autoDownloadIgnoreReUploads: Boolean! + + autoDownloadNewChapters: Boolean! + + autoDownloadNewChaptersLimit: Int! + + backupInterval: Int! + + backupPath: String! + + backupTTL: Int! + + backupTime: String! + + basicAuthEnabled: Boolean! @deprecated(reason: "Removed - prefer authMode, replace with authMode") + + basicAuthPassword: String! @deprecated(reason: "Removed - prefer authPassword, replace with authPassword") + + basicAuthUsername: String! @deprecated(reason: "Removed - prefer authUsername, replace with authUsername") + + databasePassword: String! + + databaseType: DatabaseType! + + databaseUrl: String! + + databaseUsername: String! + + debugLogsEnabled: Boolean! + + downloadAsCbz: Boolean! + + downloadConversions: [SettingsDownloadConversionType!]! + + downloadsPath: String! + + electronPath: String! + + excludeCompleted: Boolean! + + excludeEntryWithUnreadChapters: Boolean! + + excludeNotStarted: Boolean! + + excludeUnreadChapters: Boolean! + + extensionRepos: [String!]! @deprecated(reason: "Replaced with addExtensionStore and removeExtensionStore mutations") + + flareSolverrAsResponseFallback: Boolean! + + flareSolverrEnabled: Boolean! + + flareSolverrSessionName: String! + + flareSolverrSessionTtl: Int! + + flareSolverrTimeout: Int! + + flareSolverrUrl: String! + + globalUpdateInterval: Float! + + gqlDebugLogsEnabled: Boolean! @deprecated(reason: "Removed - does not do anything") + + initialOpenInBrowserEnabled: Boolean! + + ip: String! + + jwtAudience: String! + + jwtRefreshExpiry: Duration! + + jwtTokenExpiry: Duration! + + kcefEnabled: Boolean! + + koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod! + + koreaderSyncDeviceId: String! @deprecated(reason: "Moved to preference store. Is supposed to be random and gets auto generated, replace with MOVE TO PREFERENCES") + + koreaderSyncPercentageTolerance: Float! + + koreaderSyncServerUrl: String! @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncStrategy: KoreaderSyncLegacyStrategy! @deprecated(reason: "Replaced with koreaderSyncStrategyForward and koreaderSyncStrategyBackward, replace with koreaderSyncStrategyForward, koreaderSyncStrategyBackward") + + koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy! + + koreaderSyncStrategyForward: KoreaderSyncConflictStrategy! + + koreaderSyncUserkey: String! @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + koreaderSyncUsername: String! @deprecated(reason: "Moved to preference store. User is supposed to use a login/logout mutation, replace with MOVE TO PREFERENCES") + + localSourcePath: String! + + maxLogFileSize: String! + + maxLogFiles: Int! + + maxLogFolderSize: String! + + maxSourcesInParallel: Int! + + opdsCbzMimetype: CbzMediaType! + + opdsChapterSortOrder: SortOrder! + + opdsEnablePageReadProgress: Boolean! + + opdsItemsPerPage: Int! + + opdsMarkAsReadOnDownload: Boolean! + + opdsShowOnlyDownloadedChapters: Boolean! + + opdsShowOnlyUnreadChapters: Boolean! + + opdsSkipChapterMetadataFeed: Boolean! + + opdsUseBinaryFileSizes: Boolean! + + port: Int! + + serveConversions: [SettingsDownloadConversionType!]! + + socksProxyEnabled: Boolean! + + socksProxyHost: String! + + socksProxyPassword: String! + + socksProxyPort: String! + + socksProxyUsername: String! + + socksProxyVersion: Int! + + syncDataCategories: Boolean! + + syncDataChapters: Boolean! + + syncDataHistory: Boolean! + + syncDataManga: Boolean! + + syncDataTracking: Boolean! + + syncInterval: Duration! + + syncYomiApiKey: String! + + syncYomiEnabled: Boolean! + + syncYomiHost: String! + + systemTrayEnabled: Boolean! + + updateMangas: Boolean! + + useHikariConnectionPool: Boolean! + + webUIChannel: WebUIChannel! + + webUIFlavor: WebUIFlavor! + + webUIInterface: WebUIInterface! + + webUIUpdateCheckInterval: Float! +} + +type SortFilter { + default: SortSelection + + name: String! + + values: [String!]! +} + +enum SortOrder { + ASC + + DESC + + ASC_NULLS_FIRST + + DESC_NULLS_FIRST + + ASC_NULLS_LAST + + DESC_NULLS_LAST +} + +type SortSelection { + ascending: Boolean! + + index: Int! +} + +input SortSelectionInput { + ascending: Boolean! + + index: Int! +} + +input SourceConditionInput { + contentWarning: ContentWarning + + id: LongString + + isNsfw: Boolean @deprecated(reason: "replace with contentWarning == ContentRating.MIXED, replace with contentWarning") + + lang: String + + name: String +} + +type SourceEdge implements Edge { + cursor: Cursor! + + node: SourceType! +} + +input SourceFilterInput { + and: [SourceFilterInput!] + + contentWarning: ContentWarningFilterInput + + id: LongFilterInput + + isNsfw: BooleanFilterInput @deprecated(reason: "replace with contentWarning, replace with contentWarning") + + lang: StringFilterInput + + name: StringFilterInput + + not: SourceFilterInput + + or: [SourceFilterInput!] +} + +type SourceMetaType implements MetaType { + key: String! + + sourceId: LongString! + + value: String! + + source: SourceType! +} + +input SourceMetaTypeInput { + key: String! + + sourceId: LongString! + + value: String! +} + +type SourceNodeList implements NodeList { + edges: [SourceEdge!]! + + nodes: [SourceType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum SourceOrderBy { + ID + + NAME + + LANG +} + +input SourceOrderInput { + by: SourceOrderBy! + + byType: SortOrder +} + +input SourcePreferenceChangeInput { + checkBoxState: Boolean + + editTextState: String + + listState: String + + multiSelectState: [String!] + + position: Int! + + switchState: Boolean +} + +type SourceType { + baseUrl: String @deprecated(reason: ", replace with homeUrl") + + contentWarning: ContentWarning! + + displayName: String! + + homeUrl: String + + iconUrl: String! + + id: LongString! + + isConfigurable: Boolean! + + isNsfw: Boolean! @deprecated(reason: ", replace with contentWarning") + + lang: String! + + name: String! + + supportsLatest: Boolean! + + extension: ExtensionType! + + filters: [Filter!]! + + manga: MangaNodeList! + + meta: [SourceMetaType!]! + + preferences: [Preference!]! +} + +input StartDownloaderInput { + clientMutationId: String +} + +type StartDownloaderPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +input StartSyncInput { + clientMutationId: String +} + +type StartSyncPayload { + clientMutationId: String + + result: StartSyncResult! +} + +enum StartSyncResult { + SUCCESS + + SYNC_IN_PROGRESS + + SYNC_DISABLED +} + +input StopDownloaderInput { + clientMutationId: String +} + +type StopDownloaderPayload { + clientMutationId: String + + downloadStatus: DownloadStatus! +} + +""" +Built-in String +""" +scalar String + +input StringFilterInput { + distinctFrom: String + + distinctFromAll: [String!] + + distinctFromAny: [String!] + + distinctFromInsensitive: String + + distinctFromInsensitiveAll: [String!] + + distinctFromInsensitiveAny: [String!] + + endsWith: String + + endsWithAll: [String!] + + endsWithAny: [String!] + + endsWithInsensitive: String + + endsWithInsensitiveAll: [String!] + + endsWithInsensitiveAny: [String!] + + equalTo: String + + greaterThan: String + + greaterThanInsensitive: String + + greaterThanOrEqualTo: String + + greaterThanOrEqualToInsensitive: String + + in: [String!] + + inInsensitive: [String!] + + includes: String + + includesAll: [String!] + + includesAny: [String!] + + includesInsensitive: String + + includesInsensitiveAll: [String!] + + includesInsensitiveAny: [String!] + + isNull: Boolean + + lessThan: String + + lessThanInsensitive: String + + lessThanOrEqualTo: String + + lessThanOrEqualToInsensitive: String + + like: String + + likeAll: [String!] + + likeAny: [String!] + + likeInsensitive: String + + likeInsensitiveAll: [String!] + + likeInsensitiveAny: [String!] + + notDistinctFrom: String + + notDistinctFromInsensitive: String + + notEndsWith: String + + notEndsWithAll: [String!] + + notEndsWithAny: [String!] + + notEndsWithInsensitive: String + + notEndsWithInsensitiveAll: [String!] + + notEndsWithInsensitiveAny: [String!] + + notEqualTo: String + + notEqualToAll: [String!] + + notEqualToAny: [String!] + + notIn: [String!] + + notInInsensitive: [String!] + + notIncludes: String + + notIncludesAll: [String!] + + notIncludesAny: [String!] + + notIncludesInsensitive: String + + notIncludesInsensitiveAll: [String!] + + notIncludesInsensitiveAny: [String!] + + notLike: String + + notLikeAll: [String!] + + notLikeAny: [String!] + + notLikeInsensitive: String + + notLikeInsensitiveAll: [String!] + + notLikeInsensitiveAny: [String!] + + notStartsWith: String + + notStartsWithAll: [String!] + + notStartsWithAny: [String!] + + notStartsWithInsensitive: String + + notStartsWithInsensitiveAll: [String!] + + notStartsWithInsensitiveAny: [String!] + + startsWith: String + + startsWithAll: [String!] + + startsWithAny: [String!] + + startsWithInsensitive: String + + startsWithInsensitiveAll: [String!] + + startsWithInsensitiveAny: [String!] +} + +type Subscription { + downloadChanged: DownloadStatus! @deprecated(reason: "Replaced with downloadStatusChanged, replace with downloadStatusChanged(input)") + + downloadStatusChanged(input: DownloadChangedInput!): DownloadUpdates! + + webUIUpdateStatusChange: WebUIUpdateStatus! + + syncStatusChanged: SyncStatus! + + libraryUpdateStatusChanged(input: LibraryUpdateStatusChangedInput!): UpdaterUpdates! + + updateStatusChanged: UpdateStatus! @deprecated(reason: "Replaced with updates, replace with updates(input)") +} + +type SwitchPreference { + currentValue: Boolean + + default: Boolean! + + enabled: Boolean! + + key: String + + summary: String + + title: String + + visible: Boolean! +} + +type SyncConflictInfoType { + deviceName: String! + + remotePage: Int! +} + +enum SyncState { + STARTED + + CREATING_BACKUP + + DOWNLOADING + + MERGING + + UPLOADING + + RESTORING + + SUCCESS + + ERROR +} + +type SyncStatus { + backupRestoreId: String + + endDate: LongString + + errorMessage: String + + startDate: LongString! + + state: SyncState! +} + +type TextFilter { + default: String! + + name: String! +} + +input TrackProgressInput { + clientMutationId: String + + mangaId: Int! +} + +type TrackProgressPayload { + clientMutationId: String + + trackRecords: [TrackRecordType!]! +} + +input TrackRecordConditionInput { + finishDate: LongString + + id: Int + + lastChapterRead: Float + + libraryId: LongString + + mangaId: Int + + private: Boolean + + remoteId: LongString + + remoteUrl: String + + score: Float + + startDate: LongString + + status: Int + + title: String + + totalChapters: Int + + trackerId: Int +} + +type TrackRecordEdge implements Edge { + cursor: Cursor! + + node: TrackRecordType! +} + +input TrackRecordFilterInput { + and: [TrackRecordFilterInput!] + + finishDate: LongFilterInput + + id: IntFilterInput + + lastChapterRead: DoubleFilterInput + + libraryId: LongFilterInput + + mangaId: IntFilterInput + + not: TrackRecordFilterInput + + or: [TrackRecordFilterInput!] + + private: BooleanFilterInput + + remoteId: LongFilterInput + + remoteUrl: StringFilterInput + + score: DoubleFilterInput + + startDate: LongFilterInput + + status: IntFilterInput + + title: StringFilterInput + + totalChapters: IntFilterInput + + trackerId: IntFilterInput +} + +type TrackRecordNodeList implements NodeList { + edges: [TrackRecordEdge!]! + + nodes: [TrackRecordType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum TrackRecordOrderBy { + ID + + MANGA_ID + + TRACKER_ID + + REMOTE_ID + + TITLE + + LAST_CHAPTER_READ + + TOTAL_CHAPTERS + + SCORE + + START_DATE + + FINISH_DATE + + PRIVATE +} + +input TrackRecordOrderInput { + by: TrackRecordOrderBy! + + byType: SortOrder +} + +type TrackRecordType { + finishDate: LongString! + + id: Int! + + lastChapterRead: Float! + + libraryId: LongString + + mangaId: Int! + + private: Boolean! + + remoteId: LongString! + + remoteUrl: String! + + score: Float! + + startDate: LongString! + + status: Int! + + title: String! + + totalChapters: Int! + + trackerId: Int! + + displayScore: String! + + manga: MangaType! + + tracker: TrackerType! +} + +type TrackSearchType { + coverUrl: String! + + finishedReadingDate: LongString! + + id: Int! + + lastChapterRead: Float! + + libraryId: LongString + + private: Boolean! + + publishingStatus: String! + + publishingType: String! + + remoteId: LongString! + + score: Float! + + startDate: String! + + startedReadingDate: LongString! + + status: Int! + + summary: String! + + title: String! + + totalChapters: Int! + + trackerId: Int! + + trackingUrl: String! + + displayScore: String! + + tracker: TrackerType! +} + +type TrackStatusType { + name: String! + + value: Int! +} + +input TrackerConditionInput { + icon: String + + id: Int + + isLoggedIn: Boolean + + name: String +} + +type TrackerEdge implements Edge { + cursor: Cursor! + + node: TrackerType! +} + +type TrackerNodeList implements NodeList { + edges: [TrackerEdge!]! + + nodes: [TrackerType!]! + + pageInfo: PageInfo! + + totalCount: Int! +} + +enum TrackerOrderBy { + ID + + NAME + + IS_LOGGED_IN +} + +input TrackerOrderInput { + by: TrackerOrderBy! + + byType: SortOrder +} + +type TrackerType { + authUrl: String + + icon: String! + + id: Int! + + isLoggedIn: Boolean! + + name: String! + + supportsPrivateTracking: Boolean! + + supportsReadingDates: Boolean! + + supportsTrackDeletion: Boolean! + + isTokenExpired: Boolean! + + scores: [String!]! + + statuses: [TrackStatusType!]! + + trackRecords: TrackRecordNodeList! +} + +enum TriState { + IGNORE + + INCLUDE + + EXCLUDE +} + +type TriStateFilter { + default: TriState! + + name: String! +} + +input UnbindTrackInput { + clientMutationId: String + + """ + This will only work if the tracker of the track record supports deleting tracks + """ + deleteRemoteTrack: Boolean + + recordId: Int! +} + +type UnbindTrackPayload { + clientMutationId: String + + trackRecord: TrackRecordType +} + +input UpdateCategoriesInput { + clientMutationId: String + + ids: [Int!]! + + patch: UpdateCategoryPatchInput! +} + +type UpdateCategoriesPayload { + categories: [CategoryType!]! + + clientMutationId: String +} + +input UpdateCategoryInput { + clientMutationId: String + + id: Int! + + patch: UpdateCategoryPatchInput! +} + +input UpdateCategoryMangaInput { + categories: [Int!]! + + clientMutationId: String +} + +type UpdateCategoryMangaPayload { + clientMutationId: String + + updateStatus: UpdateStatus! +} + +input UpdateCategoryOrderInput { + clientMutationId: String + + id: Int! + + position: Int! +} + +type UpdateCategoryOrderPayload { + categories: [CategoryType!]! + + clientMutationId: String +} + +input UpdateCategoryPatchInput { + default: Boolean + + includeInDownload: IncludeOrExclude + + includeInUpdate: IncludeOrExclude + + name: String +} + +type UpdateCategoryPayload { + category: CategoryType! + + clientMutationId: String +} + +input UpdateChapterInput { + clientMutationId: String + + id: Int! + + patch: UpdateChapterPatchInput! +} + +input UpdateChapterPatchInput { + isBookmarked: Boolean + + isRead: Boolean + + lastPageRead: Int +} + +type UpdateChapterPayload { + chapter: ChapterType! + + clientMutationId: String +} + +input UpdateChaptersInput { + clientMutationId: String + + ids: [Int!]! + + patch: UpdateChapterPatchInput! +} + +type UpdateChaptersPayload { + chapters: [ChapterType!]! + + clientMutationId: String +} + +input UpdateExtensionInput { + clientMutationId: String + + id: String! + + patch: UpdateExtensionPatchInput! +} + +input UpdateExtensionPatchInput { + install: Boolean + + uninstall: Boolean + + update: Boolean +} + +type UpdateExtensionPayload { + clientMutationId: String + + extension: ExtensionType +} + +input UpdateExtensionsInput { + clientMutationId: String + + ids: [String!]! + + patch: UpdateExtensionPatchInput! +} + +type UpdateExtensionsPayload { + clientMutationId: String + + extensions: [ExtensionType!]! +} + +input UpdateLibraryInput { + categories: [Int!] + + clientMutationId: String +} + +input UpdateLibraryMangaInput { + clientMutationId: String +} + +type UpdateLibraryMangaPayload { + clientMutationId: String + + updateStatus: UpdateStatus! +} + +type UpdateLibraryPayload { + clientMutationId: String + + updateStatus: LibraryUpdateStatus! +} + +input UpdateMangaCategoriesInput { + clientMutationId: String + + id: Int! + + patch: UpdateMangaCategoriesPatchInput! +} + +input UpdateMangaCategoriesPatchInput { + addToCategories: [Int!] + + clearCategories: Boolean + + removeFromCategories: [Int!] +} + +type UpdateMangaCategoriesPayload { + clientMutationId: String + + manga: MangaType! +} + +input UpdateMangaInput { + clientMutationId: String + + id: Int! + + patch: UpdateMangaPatchInput! +} + +input UpdateMangaPatchInput { + inLibrary: Boolean +} + +type UpdateMangaPayload { + clientMutationId: String + + manga: MangaType! +} + +input UpdateMangasCategoriesInput { + clientMutationId: String + + ids: [Int!]! + + patch: UpdateMangaCategoriesPatchInput! +} + +type UpdateMangasCategoriesPayload { + clientMutationId: String + + mangas: [MangaType!]! +} + +input UpdateMangasInput { + clientMutationId: String + + ids: [Int!]! + + patch: UpdateMangaPatchInput! +} + +type UpdateMangasPayload { + clientMutationId: String + + mangas: [MangaType!]! +} + +input UpdateSourcePreferenceInput { + change: SourcePreferenceChangeInput! + + clientMutationId: String + + source: LongString! +} + +type UpdateSourcePreferencePayload { + clientMutationId: String + + preferences: [Preference!]! + + source: SourceType! +} + +enum UpdateState { + IDLE + + DOWNLOADING + + FINISHED + + ERROR +} + +type UpdateStatus { + completeJobs: UpdateStatusType! + + failedJobs: UpdateStatusType! + + isRunning: Boolean! + + pendingJobs: UpdateStatusType! + + runningJobs: UpdateStatusType! + + skippedCategories: UpdateStatusCategoryType! + + skippedJobs: UpdateStatusType! + + updatingCategories: UpdateStatusCategoryType! +} + +type UpdateStatusCategoryType { + categories: CategoryNodeList! +} + +type UpdateStatusType { + mangas: MangaNodeList! +} + +input UpdateStopInput { + clientMutationId: String +} + +type UpdateStopPayload { + clientMutationId: String +} + +enum UpdateStrategy { + ALWAYS_UPDATE + + ONLY_FETCH_ONCE +} + +input UpdateTrackInput { + clientMutationId: String + + """ + This will only work if the tracker of the track record supports reading dates + """ + finishDate: LongString + + lastChapterRead: Float + + """ + This will only work if the tracker of the track record supports private tracking + """ + private: Boolean + + recordId: Int! + + scoreString: String + + """ + This will only work if the tracker of the track record supports reading dates + """ + startDate: LongString + + status: Int + + unbind: Boolean @deprecated(reason: "Replaced with \"unbindTrack\" mutation, replace with unbindTrack") +} + +type UpdateTrackPayload { + clientMutationId: String + + trackRecord: TrackRecordType +} + +type UpdaterJobsInfoType { + finishedJobs: Int! + + isRunning: Boolean! + + skippedCategoriesCount: Int! + + skippedMangasCount: Int! + + totalJobs: Int! +} + +type UpdaterUpdates { + categoryUpdates: [CategoryUpdateType!]! + + """ + The current update status at the time of sending the initial message. Is null for all following messages + """ + initial: LibraryUpdateStatus + + jobsInfo: UpdaterJobsInfoType! + + mangaUpdates: [MangaUpdateType!]! + + """ + Indicates whether updates have been omitted based on the "maxUpdates" subscription variable. In case updates have been omitted, the "updateStatus" query should be re-fetched. + """ + omittedUpdates: Boolean! +} + +""" +A file part in a multipart request +""" +scalar Upload + +input ValidateBackupInput { + backup: Upload! +} + +type ValidateBackupResult { + missingSources: [ValidateBackupSource!]! + + missingTrackers: [ValidateBackupTracker!]! +} + +type ValidateBackupSource { + id: LongString! + + name: String! +} + +type ValidateBackupTracker { + name: String! +} + +enum WebUIChannel { + BUNDLED + + STABLE + + PREVIEW +} + +enum WebUIFlavor { + WEBUI + + VUI + + CUSTOM +} + +enum WebUIInterface { + BROWSER + + ELECTRON +} + +type WebUIUpdateCheck { + channel: WebUIChannel! + + tag: String! + + updateAvailable: Boolean! +} + +type WebUIUpdateInfo { + channel: WebUIChannel! + + tag: String! +} + +input WebUIUpdateInput { + clientMutationId: String +} + +type WebUIUpdatePayload { + clientMutationId: String + + updateStatus: WebUIUpdateStatus! +} + +type WebUIUpdateStatus { + info: WebUIUpdateInfo! + + progress: Int! + + state: UpdateState! +} + +type __Directive { + """ + The __Directive type represents a Directive that a server supports. + """ + name: String! + + description: String + + isRepeatable: Boolean! + + locations: [__DirectiveLocation!]! + + args(includeDeprecated: Boolean = false): [__InputValue!]! +} + +""" +An enum describing valid locations where a directive can be placed +""" +enum __DirectiveLocation { + """ + Indicates the directive is valid on queries. + """ + QUERY + + """ + Indicates the directive is valid on mutations. + """ + MUTATION + + """ + Indicates the directive is valid on subscriptions. + """ + SUBSCRIPTION + + """ + Indicates the directive is valid on fields. + """ + FIELD + + """ + Indicates the directive is valid on fragment definitions. + """ + FRAGMENT_DEFINITION + + """ + Indicates the directive is valid on fragment spreads. + """ + FRAGMENT_SPREAD + + """ + Indicates the directive is valid on inline fragments. + """ + INLINE_FRAGMENT + + """ + Indicates the directive is valid on variable definitions. + """ + VARIABLE_DEFINITION + + """ + Indicates the directive is valid on a schema SDL definition. + """ + SCHEMA + + """ + Indicates the directive is valid on a scalar SDL definition. + """ + SCALAR + + """ + Indicates the directive is valid on an object SDL definition. + """ + OBJECT + + """ + Indicates the directive is valid on a field SDL definition. + """ + FIELD_DEFINITION + + """ + Indicates the directive is valid on a field argument SDL definition. + """ + ARGUMENT_DEFINITION + + """ + Indicates the directive is valid on an interface SDL definition. + """ + INTERFACE + + """ + Indicates the directive is valid on an union SDL definition. + """ + UNION + + """ + Indicates the directive is valid on an enum SDL definition. + """ + ENUM + + """ + Indicates the directive is valid on an enum value SDL definition. + """ + ENUM_VALUE + + """ + Indicates the directive is valid on an input object SDL definition. + """ + INPUT_OBJECT + + """ + Indicates the directive is valid on an input object field SDL definition. + """ + INPUT_FIELD_DEFINITION +} + +type __EnumValue { + name: String! + + description: String + + isDeprecated: Boolean! + + deprecationReason: String +} + +type __Field { + name: String! + + description: String + + args(includeDeprecated: Boolean = false): [__InputValue!]! + + type: __Type! + + isDeprecated: Boolean! + + deprecationReason: String +} + +type __InputValue { + name: String! + + description: String + + type: __Type! + + defaultValue: String + + isDeprecated: Boolean + + deprecationReason: String +} + +""" +A GraphQL Introspection defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, the entry points for query, mutation, and subscription operations. +""" +type __Schema { + description: String + + """ + A list of all types supported by this server. + """ + types: [__Type!]! + + """ + The type that query operations will be rooted at. + """ + queryType: __Type! + + """ + If this server supports mutation, the type that mutation operations will be rooted at. + """ + mutationType: __Type + + """ + A list of all directives supported by this server. + """ + directives: [__Directive!]! + + """ + If this server support subscription, the type that subscription operations will be rooted at. + """ + subscriptionType: __Type +} + +type __Type { + kind: __TypeKind! + + name: String + + description: String + + fields(includeDeprecated: Boolean = false): [__Field!] + + interfaces: [__Type!] + + possibleTypes: [__Type!] + + enumValues(includeDeprecated: Boolean = false): [__EnumValue!] + + inputFields(includeDeprecated: Boolean = false): [__InputValue!] + + ofType: __Type + + """ + This field is considered experimental because it has not yet been ratified in the graphql specification + """ + isOneOf: Boolean + + specifiedByURL: String + + specifiedByUrl: String @deprecated(reason: "This legacy name has been replaced by `specifiedByURL`") +} + +""" +An enum describing what kind of type a given __Type is +""" +enum __TypeKind { + """ + Indicates this type is a scalar. `specifiedByURL` is a valid field + """ + SCALAR + + """ + Indicates this type is an object. `fields` and `interfaces` are valid fields. + """ + OBJECT + + """ + Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + """ + INTERFACE + + """ + Indicates this type is a union. `possibleTypes` is a valid field. + """ + UNION + + """ + Indicates this type is an enum. `enumValues` is a valid field. + """ + ENUM + + """ + Indicates this type is an input object. `inputFields` is a valid field. + """ + INPUT_OBJECT + + """ + Indicates this type is a list. `ofType` is a valid field. + """ + LIST + + """ + Indicates this type is a non-null. `ofType` is a valid field. + """ + NON_NULL +} + +""" +Directs the executor to include this field or fragment only when the `if` argument is true +""" +directive @include("Included when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip("Skipped when true." if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Requires user authentication +""" +directive @requireAuth on OBJECT | FIELD_DEFINITION + +""" +Marks the field, argument, input field or enum value as deprecated +""" +directive @deprecated("The reason for the deprecation" reason: String! = "No longer supported") on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION + +""" +Exposes a URL that specifies the behaviour of this scalar. +""" +directive @specifiedBy("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR + +""" +Indicates an Input Object is a OneOf Input Object. +""" +directive @oneOf on INPUT_OBJECT + +""" +This directive allows results to be deferred during execution +""" +directive @defer("Deferred behaviour is controlled by this argument" if: Boolean! = true, "A unique label that represents the fragment being deferred" label: String) on FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +This directive disables error propagation when a non nullable field returns null for the given operation. +""" +directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION + +schema { + query: Query + mutation: Mutation + subscription: Subscription +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt index eba6717da..80c3ece80 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/Anilist.kt @@ -207,7 +207,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { } override suspend fun refresh(track: Track): Track { - val remoteTrack = api.getLibManga(track, getUsername().toInt()) + val remoteTrack = api.findLibManga(track, getUsername().toInt()) ?: throw Exception("Could not find manga") track.copyPersonalFrom(remoteTrack) track.title = remoteTrack.title track.total_chapters = remoteTrack.total_chapters @@ -216,7 +216,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { override suspend fun updateUserConfig() { val currentUser = api.getCurrentUser() - scorePreference.set(currentUser.mediaListOptions.scoreFormat) + scorePreference.set(currentUser.scoreFormat) saveDisplayUsername(currentUser.name) } @@ -227,7 +227,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker { val oauth = ALOAuth(token) interceptor.setAuth(oauth) val currentUser = api.getCurrentUser() - scorePreference.set(currentUser.mediaListOptions.scoreFormat) + scorePreference.set(currentUser.scoreFormat) saveDisplayUsername(currentUser.name) saveCredentials(currentUser.id.toString(), oauth.accessToken) } catch (_: Throwable) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt index ba47e3854..d2cf66fa2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistApi.kt @@ -2,31 +2,28 @@ package eu.kanade.tachiyomi.data.track.anilist import android.net.Uri import androidx.core.net.toUri +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.network.okHttpClient import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult -import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult -import eu.kanade.tachiyomi.data.track.anilist.dto.ALSearchResult -import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserListMangaQueryResult -import eu.kanade.tachiyomi.data.track.anilist.dto.ALUserViewerData +import eu.kanade.tachiyomi.data.track.anilist.dto.ALUser import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.dataOrElse import eu.kanade.tachiyomi.network.interceptor.rateLimit -import eu.kanade.tachiyomi.network.jsonMime -import eu.kanade.tachiyomi.network.parseAs +import kotlinx.coroutines.Dispatchers import kotlinx.datetime.TimeZone import kotlinx.datetime.number import kotlinx.datetime.toLocalDateTime -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonNull -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject +import mihon.graphql.anilist.AniListAddMangaMutation +import mihon.graphql.anilist.AniListDeleteMangaMutation +import mihon.graphql.anilist.AniListGetCurrentUserQuery +import mihon.graphql.anilist.AniListGetLibMangaQuery +import mihon.graphql.anilist.AniListGetMangaDetailsQuery +import mihon.graphql.anilist.AniListSearchMangaQuery +import mihon.graphql.anilist.AniListUpdateMangaMutation +import mihon.graphql.anilist.type.FuzzyDateInput import okhttp3.OkHttpClient -import okhttp3.RequestBody.Companion.toRequestBody -import tachiyomi.core.common.util.lang.withIOContext -import uy.kohesive.injekt.injectLazy +import tachiyomi.core.common.util.system.logcat import kotlin.time.Duration.Companion.minutes import kotlin.time.Instant import tachiyomi.domain.track.model.Track as DomainTrack @@ -37,381 +34,175 @@ class AnilistApi( interceptor: AnilistInterceptor, ) { - private val json: Json by injectLazy() - private val authClient = client.newBuilder() .addInterceptor(interceptor) .rateLimit(permits = 25, period = 1.minutes) .build() + private val graphQlClient by lazy { + ApolloClient.Builder() + .serverUrl("https://graphql.anilist.co") + .okHttpClient(authClient) + .dispatcher(Dispatchers.IO) + // required to log the error body in dataOrElse, which also properly closes it + .httpExposeErrorBody(true) + .build() + } + suspend fun addLibManga(track: Track): Track { - return withIOContext { - val query = $$""" - |mutation AddManga($mangaId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean) { - |SaveMediaListEntry (mediaId: $mangaId, progress: $progress, status: $status, private: $private) { - | id - | status - |} - |} - | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("mangaId", track.remote_id) - put("progress", track.last_chapter_read.toInt()) - put("status", track.toApiStatus()) - put("private", track.private) + return graphQlClient + .mutation( + AniListAddMangaMutation( + manga_id = track.remote_id.toInt(), + progress = track.last_chapter_read.toInt(), + status = track.toApiStatus(), + private = track.private, + ), + ) + .execute() + .dataOrElse( + errorLog = "AniList: Failed to add manga", + default = { null }, + ) { + it.SaveMediaListEntry?.id?.let { libraryId -> + track.library_id = libraryId.toLong() + track } } - with(json) { - authClient.newCall( - POST( - API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .let { - track.library_id = it.data.entry.id - track - } - } - } + ?: throw Exception("Failed to add manga") } suspend fun updateLibManga(track: Track): Track { - return withIOContext { - val query = $$""" - |mutation UpdateManga( - |$listId: Int, $progress: Int, $status: MediaListStatus, $private: Boolean, - |$score: Int, $startedAt: FuzzyDateInput, $completedAt: FuzzyDateInput - |) { - |SaveMediaListEntry( - |id: $listId, progress: $progress, status: $status, private: $private, - |scoreRaw: $score, startedAt: $startedAt, completedAt: $completedAt - |) { - |id - |status - |progress - |} - |} - | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("listId", track.library_id) - put("progress", track.last_chapter_read.toInt()) - put("status", track.toApiStatus()) - put("score", track.score.toInt()) - put("startedAt", createDate(track.started_reading_date)) - put("completedAt", createDate(track.finished_reading_date)) - put("private", track.private) + val libraryId = track.library_id + requireNotNull(libraryId) { "AniList cannot update track with null library_id" } + + return graphQlClient + .mutation( + AniListUpdateMangaMutation( + library_id = libraryId.toInt(), + progress = track.last_chapter_read.toInt(), + status = track.toApiStatus(), + private = track.private, + score = track.score.toInt(), + startedAt = createFuzzyDate(track.started_reading_date), + completedAt = createFuzzyDate(track.finished_reading_date), + ), + ) + .execute() + .dataOrElse( + errorLog = "AniList: Failed to update manga", + default = { null }, + ) { + it.SaveMediaListEntry?.id?.let { remoteLibraryId -> + track.library_id = remoteLibraryId.toLong() + track } } - authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - track - } + ?: throw Exception("Failed to update manga") } suspend fun deleteLibManga(track: DomainTrack) { - withIOContext { - val query = $$""" - |mutation DeleteManga($listId: Int) { - |DeleteMediaListEntry(id: $listId) { - |deleted - |} - |} - | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("listId", track.libraryId) + val libraryId = track.libraryId + requireNotNull(libraryId) { "AniList cannot delete track with null library_id" } + + graphQlClient + .mutation( + AniListDeleteMangaMutation(library_id = libraryId.toInt()), + ) + .execute() + .dataOrElse( + errorLog = "AniList: Failed to delete manga", + default = { null }, + ) { + it.DeleteMediaListEntry?.deleted?.let { deleted -> + if (deleted) { + logcat { "AniList: Deleted manga ${track.libraryId} successfully" } + } } } - authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) - .awaitSuccess() - } + ?: throw Exception("Failed to delete manga") } suspend fun search(search: String): List { - return withIOContext { - val query = $$""" - |query Search($query: String) { - |Page (perPage: 50) { - |media(search: $query, type: MANGA, format_not_in: [NOVEL]) { - |id - |staff { - |edges { - |role - |id - |node { - |name { - |full - |userPreferred - |native - |} - |} - |} - |} - |title { - |userPreferred - |} - |coverImage { - |large - |} - |format - |countryOfOrigin - |status - |chapters - |description - |startDate { - |year - |month - |day - |} - |averageScore - |} - |} - |} - | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("query", search) - } + return graphQlClient + .query( + AniListSearchMangaQuery(search = search), + ) + .execute() + .dataOrElse( + errorLog = "AniList: Search failed", + default = { emptyList() }, + ) { + it.Page?.media + ?.mapNotNull { alManga -> alManga?.toTrackSearch(trackerId) } + ?: emptyList() } - with(json) { - authClient.newCall( - POST( - API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.page.media - .map { it.toALManga().toTrack(trackerId) } - } - } } - suspend fun findLibManga(track: Track, userid: Int): Track? { - return withIOContext { - val query = $$""" - |query ($id: Int!, $manga_id: Int!) { - |Page { - |mediaList(userId: $id, type: MANGA, mediaId: $manga_id) { - |id - |status - |scoreRaw: score(format: POINT_100) - |progress - |private - |startedAt { - |year - |month - |day - |} - |completedAt { - |year - |month - |day - |} - |media { - |id - |title { - |userPreferred - |} - |coverImage { - |large - |} - |format - |status - |chapters - |description - |startDate { - |year - |month - |day - |} - |staff { - |edges { - |role - |id - |node { - |name { - |full - |userPreferred - |native - |} - |} - |} - |} - |} - |} - |} - |} - | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("id", userid) - put("manga_id", track.remote_id) - } - } - with(json) { - authClient.newCall( - POST( - API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.page.mediaList - .map { it.toALUserManga() } - .firstOrNull() + suspend fun findLibManga(track: Track, userId: Int): Track? { + return graphQlClient + .query( + AniListGetLibMangaQuery( + user_id = userId, + manga_id = track.remote_id.toInt(), + ), + ) + .execute() + .dataOrElse( + errorLog = "AniList: Failed to find manga in library", + default = { null }, + ) { + it.Page?.mediaList + ?.firstOrNull() ?.toTrack(trackerId) } - } } - suspend fun getLibManga(track: Track, userId: Int): Track { - return findLibManga(track, userId) ?: throw Exception("Could not find manga") - } - - suspend fun getCurrentUser(): ALUserViewerData { - return withIOContext { - val query = """ - |query User { - |Viewer { - |id - |name - |mediaListOptions { - |scoreFormat - |} - |} - |} - | - """.trimMargin() - val payload = buildJsonObject { - put("query", query) + suspend fun getCurrentUser(): ALUser { + return graphQlClient + .query(AniListGetCurrentUserQuery()) + .execute() + .dataOrElse( + errorLog = "AniList: Failed to get current user", + default = { null }, + ) { + it.Viewer?.toALUser() } - with(json) { - authClient.newCall( - POST( - API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.viewer - } - } + ?: throw Exception("Failed to get AniList user data") } suspend fun getMangaDetails(id: Int): TrackSearch? { - return withIOContext { - val query = $$""" - |query Search($manga_id: Int) { - |Page (perPage: 1) { - |media(id: $manga_id, type: MANGA, format_not_in: [NOVEL]) { - |id - |staff { - |edges { - |role - |id - |node { - |name { - |full - |userPreferred - |native - |} - |} - |} - |} - |title { - |userPreferred - |} - |coverImage { - |large - |} - |format - |countryOfOrigin - |status - |chapters - |description - |startDate { - |year - |month - |day - |} - |averageScore - |} - |} - |} - | - """.trimMargin() - - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("manga_id", id) - } + return graphQlClient + .query( + AniListGetMangaDetailsQuery(manga_id = id), + ) + .execute() + .dataOrElse( + errorLog = "AniList: Failed to get manga details", + default = { null }, + ) { + it.Page?.media + ?.firstOrNull() + ?.toTrackSearch(trackerId) } - - with(json) { - authClient.newCall( - POST( - API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.page.media - .firstOrNull() - ?.toALManga() - ?.toTrack(trackerId) - } - } } - private fun createDate(dateValue: Long): JsonObject { - if (dateValue == 0L) { - return buildJsonObject { - put("year", JsonNull) - put("month", JsonNull) - put("day", JsonNull) - } - } + private fun createFuzzyDate(dateValue: Long): FuzzyDateInput { + // all absent/null + if (dateValue == 0L) return FuzzyDateInput() val dateTime = Instant.fromEpochMilliseconds(dateValue).toLocalDateTime(TimeZone.currentSystemDefault()) - return buildJsonObject { - put("year", dateTime.year) - put("month", dateTime.month.number) - put("day", dateTime.day) - } + return FuzzyDateInput( + year = Optional.present(dateTime.year), + month = Optional.present(dateTime.month.number), + day = Optional.present(dateTime.day), + ) } companion object { private const val CLIENT_ID = "16329" - private const val API_URL = "https://graphql.anilist.co/" - private const val BASE_URL = "https://anilist.co/api/v2/" - private const val BASE_MANGA_URL = "https://anilist.co/manga/" - fun mangaUrl(mediaId: Long): String { - return BASE_MANGA_URL + mediaId - } - - fun authUrl(): Uri = "${BASE_URL}oauth/authorize".toUri().buildUpon() + fun authUrl(): Uri = "https://anilist.co/api/v2/oauth/authorize".toUri().buildUpon() .appendQueryParameter("client_id", CLIENT_ID) .appendQueryParameter("response_type", "token") .build() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt index 3fc94f144..b60d2eaa6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistUtils.kt @@ -2,15 +2,130 @@ package eu.kanade.tachiyomi.data.track.anilist import eu.kanade.domain.track.service.TrackPreferences import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.anilist.dto.ALUser +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.util.lang.htmlDecode +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.atStartOfDayIn +import mihon.graphql.anilist.AniListGetCurrentUserQuery +import mihon.graphql.anilist.AniListGetLibMangaQuery +import mihon.graphql.anilist.AniListGetMangaDetailsQuery +import mihon.graphql.anilist.AniListSearchMangaQuery +import mihon.graphql.anilist.fragment.MangaFragment +import mihon.graphql.anilist.type.MediaListStatus +import mihon.graphql.anilist.type.ScoreFormat import tachiyomi.domain.track.model.Track as DomainTrack -internal fun Track.toApiStatus() = when (status) { - Anilist.READING -> "CURRENT" - Anilist.COMPLETED -> "COMPLETED" - Anilist.ON_HOLD -> "PAUSED" - Anilist.DROPPED -> "DROPPED" - Anilist.PLAN_TO_READ -> "PLANNING" - Anilist.REREADING -> "REPEATING" +internal fun AniListGetCurrentUserQuery.Viewer.toALUser(): ALUser { + return ALUser( + id = id, + name = name, + scoreFormat = mediaListOptions?.scoreFormat?.rawValue ?: ScoreFormat.POINT_10_DECIMAL.rawValue, + ) +} + +internal fun AniListGetLibMangaQuery.MediaList.toTrack(trackId: Long): Track { + val mediaList = this + requireNotNull(mediaList.media) { "Missing Media data from AniList" } + + return mediaList.media.mangaFragment + .toTrackSearch(trackId) + .apply { + library_id = mediaList.id.toLong() + status = when (mediaList.status) { + MediaListStatus.CURRENT -> Anilist.READING + MediaListStatus.COMPLETED -> Anilist.COMPLETED + MediaListStatus.PAUSED -> Anilist.ON_HOLD + MediaListStatus.DROPPED -> Anilist.DROPPED + MediaListStatus.PLANNING -> Anilist.PLAN_TO_READ + MediaListStatus.REPEATING -> Anilist.REREADING + else -> throw NotImplementedError("Unknown status: ${mediaList.status?.rawValue}") + } + score = mediaList.scoreRaw ?: 0.0 + last_chapter_read = mediaList.progress?.toDouble() ?: 0.0 + + started_reading_date = mediaList.startedAt.let { + if (it == null || it.year == null || it.month == null || it.day == null) { + 0 + } else { + LocalDate(it.year, it.month, it.day) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + .toEpochMilliseconds() + } + } + + finished_reading_date = mediaList.completedAt.let { + if (it == null || it.year == null || it.month == null || it.day == null) { + 0 + } else { + LocalDate(it.year, it.month, it.day) + .atStartOfDayIn(TimeZone.currentSystemDefault()) + .toEpochMilliseconds() + } + } + + private = mediaList.private ?: false + } +} + +internal fun AniListSearchMangaQuery.Medium.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +internal fun AniListGetMangaDetailsQuery.Medium.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +private fun MangaFragment.toTrackSearch(trackId: Long): TrackSearch { + val alManga = this + return TrackSearch.create(trackId).apply { + remote_id = alManga.id.toLong() + title = alManga.title?.userPreferred ?: "(no title)" + total_chapters = alManga.chapters?.toLong() ?: 0 + cover_url = alManga.coverImage?.large ?: "" + summary = alManga.description?.htmlDecode() ?: "" + score = alManga.averageScore?.toDouble() ?: -1.0 + tracking_url = "https://anilist.co/manga/$remote_id" + publishing_status = alManga.status?.rawValue ?: "" + publishing_type = if (alManga.format == null) { + "Manga" + } else if (alManga.format.rawValue != "MANGA") { + alManga.format.rawValue.replace("_", "-") + } else { + when (alManga.countryOfOrigin) { + "KR" -> "Manhwa" + "CN", "TW" -> "Manhua" + else -> "Manga" + } + } + start_date = alManga.startDate?.let { + buildString { + it.year?.let { year -> append(year) } + it.month?.let { month -> append("-$month") } + it.day?.let { day -> append("-$day") } + }.trim('-') + } ?: "" + + authors = alManga.staff?.edges + ?.filter { "Story" in (it?.role ?: "") } + ?.mapNotNull { it?.node?.name?.userPreferred } + ?: emptyList() + + artists = alManga.staff?.edges + ?.filter { "Art" in (it?.role ?: "") } + ?.mapNotNull { it?.node?.name?.userPreferred } + ?: emptyList() + } +} + +internal fun Track.toApiStatus(): MediaListStatus = when (status) { + Anilist.READING -> MediaListStatus.CURRENT + Anilist.COMPLETED -> MediaListStatus.COMPLETED + Anilist.ON_HOLD -> MediaListStatus.PAUSED + Anilist.DROPPED -> MediaListStatus.DROPPED + Anilist.PLAN_TO_READ -> MediaListStatus.PLANNING + Anilist.REREADING -> MediaListStatus.REPEATING else -> throw NotImplementedError("Unknown status: $status") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt deleted file mode 100644 index a552e7c1d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALAddManga.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.dto - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ALAddMangaResult( - val data: ALAddMangaData, -) - -@Serializable -data class ALAddMangaData( - @SerialName("SaveMediaListEntry") - val entry: ALAddMangaEntry, -) - -@Serializable -data class ALAddMangaEntry( - val id: Long, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt deleted file mode 100644 index 1ae59602e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALFuzzyDate.kt +++ /dev/null @@ -1,21 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.dto - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.serialization.Serializable - -@Serializable -data class ALFuzzyDate( - val year: Int?, - val month: Int?, - val day: Int?, -) { - fun toEpochMilli(): Long = try { - LocalDate(year!!, month!!, day!!) - .atStartOfDayIn(TimeZone.currentSystemDefault()) - .toEpochMilliseconds() - } catch (_: Exception) { - 0L - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt deleted file mode 100644 index fa3fe84a0..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALManga.kt +++ /dev/null @@ -1,81 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.dto - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.anilist.Anilist -import eu.kanade.tachiyomi.data.track.anilist.AnilistApi -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.util.lang.htmlDecode -import java.text.SimpleDateFormat -import java.util.Locale - -data class ALManga( - val remoteId: Long, - val title: String, - val imageUrl: String, - val description: String?, - val format: String, - val publishingStatus: String, - val startDateFuzzy: Long, - val totalChapters: Long, - val averageScore: Int, - val staff: ALStaff, -) { - fun toTrack(trackerId: Long) = TrackSearch.create(trackerId).apply { - remote_id = remoteId - title = this@ALManga.title - total_chapters = totalChapters - cover_url = imageUrl - summary = description?.htmlDecode() ?: "" - score = averageScore.toDouble() - tracking_url = AnilistApi.mangaUrl(remote_id) - publishing_status = publishingStatus - publishing_type = format - if (startDateFuzzy != 0L) { - start_date = try { - val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US) - outputDf.format(startDateFuzzy) - } catch (e: IllegalArgumentException) { - "" - } - } - staff.edges.forEach { - val name = it.node.name() ?: return@forEach - if ("Story" in it.role) authors += name - if ("Art" in it.role) artists += name - } - } -} - -data class ALUserManga( - val libraryId: Long, - val listStatus: String, - val scoreRaw: Int, - val chaptersRead: Int, - val startDateFuzzy: Long, - val completedDateFuzzy: Long, - val manga: ALManga, - val private: Boolean, -) { - fun toTrack(trackerId: Long) = Track.create(trackerId).apply { - remote_id = manga.remoteId - title = manga.title - status = toTrackStatus() - score = scoreRaw.toDouble() - started_reading_date = startDateFuzzy - finished_reading_date = completedDateFuzzy - last_chapter_read = chaptersRead.toDouble() - library_id = libraryId - total_chapters = manga.totalChapters - private = this@ALUserManga.private - } - - private fun toTrackStatus() = when (listStatus) { - "CURRENT" -> Anilist.READING - "COMPLETED" -> Anilist.COMPLETED - "PAUSED" -> Anilist.ON_HOLD - "DROPPED" -> Anilist.DROPPED - "PLANNING" -> Anilist.PLAN_TO_READ - "REPEATING" -> Anilist.REREADING - else -> throw NotImplementedError("Unknown status: $listStatus") - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt deleted file mode 100644 index f13ebb400..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearch.kt +++ /dev/null @@ -1,20 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.dto - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ALSearchResult( - val data: ALSearchPage, -) - -@Serializable -data class ALSearchPage( - @SerialName("Page") - val page: ALSearchMedia, -) - -@Serializable -data class ALSearchMedia( - val media: List, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt deleted file mode 100644 index 8aebb2087..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALSearchItem.kt +++ /dev/null @@ -1,77 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class ALSearchItem( - val id: Long, - val title: ALItemTitle, - val coverImage: ItemCover, - val description: String?, - val format: String, - val status: String?, - val startDate: ALFuzzyDate, - val chapters: Long?, - val averageScore: Int?, - val staff: ALStaff, - val countryOfOrigin: String = "", -) { - fun toALManga(): ALManga = ALManga( - remoteId = id, - title = title.userPreferred, - imageUrl = coverImage.large, - description = description, - format = if (format != "MANGA") { - format.replace("_", "-") - } else { - when (countryOfOrigin) { - "KR" -> "Manhwa" - "CN", "TW" -> "Manhua" - else -> "Manga" - } - }, - publishingStatus = status ?: "", - startDateFuzzy = startDate.toEpochMilli(), - totalChapters = chapters ?: 0, - averageScore = averageScore ?: -1, - staff = staff, - ) -} - -@Serializable -data class ALItemTitle( - val userPreferred: String, -) - -@Serializable -data class ItemCover( - val large: String, -) - -@Serializable -data class ALStaff( - val edges: List, -) - -@Serializable -data class ALEdge( - val role: String, - val id: Int, - val node: ALStaffNode, -) - -@Serializable -data class ALStaffNode( - val name: ALStaffName, -) - -@Serializable -data class ALStaffName( - val userPreferred: String?, - val native: String?, - val full: String?, -) { - operator fun invoke(): String? { - return userPreferred ?: full ?: native - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt index 6387a8d44..d4fdc1f6e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUser.kt @@ -1,27 +1,10 @@ package eu.kanade.tachiyomi.data.track.anilist.dto -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class ALCurrentUserResult( - val data: ALUserViewer, -) - -@Serializable -data class ALUserViewer( - @SerialName("Viewer") - val viewer: ALUserViewerData, -) - -@Serializable -data class ALUserViewerData( +data class ALUser( val id: Int, val name: String, - val mediaListOptions: ALUserListOptions, -) - -@Serializable -data class ALUserListOptions( val scoreFormat: String, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt deleted file mode 100644 index 552177a25..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/dto/ALUserList.kt +++ /dev/null @@ -1,45 +0,0 @@ -package eu.kanade.tachiyomi.data.track.anilist.dto - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ALUserListMangaQueryResult( - val data: ALUserListMangaPage, -) - -@Serializable -data class ALUserListMangaPage( - @SerialName("Page") - val page: ALUserListMediaList, -) - -@Serializable -data class ALUserListMediaList( - val mediaList: List, -) - -@Serializable -data class ALUserListItem( - val id: Long, - val status: String, - val scoreRaw: Int, - val progress: Int, - val startedAt: ALFuzzyDate, - val completedAt: ALFuzzyDate, - val media: ALSearchItem, - val private: Boolean, -) { - fun toALUserManga(): ALUserManga { - return ALUserManga( - libraryId = this@ALUserListItem.id, - listStatus = status, - scoreRaw = scoreRaw, - chaptersRead = progress, - startDateFuzzy = startedAt.toEpochMilli(), - completedDateFuzzy = completedAt.toEpochMilli(), - manga = media.toALManga(), - private = private, - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt index 71813398c..a309cbb34 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/Kitsu.kt @@ -192,7 +192,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { logcat(LogPriority.ERROR) { "Unsupported Kitsu score type: $ratingSystem" } scorePreference.set(RATING_ADVANCED) } - saveDisplayUsername(currentUser.profile.name) + saveDisplayUsername(currentUser.name) saveCredentials(username, currentUser.id) } @@ -206,7 +206,7 @@ class Kitsu(id: Long) : BaseTracker(id, "Kitsu"), DeletableTracker { logcat(LogPriority.ERROR) { "Unsupported Kitsu score type: $ratingSystem" } scorePreference.set(RATING_ADVANCED) } - saveDisplayUsername(currentUser.profile.name) + saveDisplayUsername(currentUser.name) } override fun logout() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt index d65176572..380f3c822 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuApi.kt @@ -1,31 +1,30 @@ package eu.kanade.tachiyomi.data.track.kitsu +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.network.okHttpClient import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAccount -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuAddMangaResult -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuCurrentAccountResult -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuDeleteMangaResult import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchByIdResult -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchByIdWithLibraryResult -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchBySlugResult -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuSearchByTitleResult -import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuUpdateMangaResult +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuUser import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.network.HttpException import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.jsonMime +import eu.kanade.tachiyomi.network.dataOrElse import eu.kanade.tachiyomi.network.parseAs +import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.Json -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonObject -import logcat.LogPriority +import mihon.graphql.kitsu.KitsuAddLibMangaMutation +import mihon.graphql.kitsu.KitsuDeleteLibEntryMutation +import mihon.graphql.kitsu.KitsuFindLibMangaQuery +import mihon.graphql.kitsu.KitsuGetCurrentAccountQuery +import mihon.graphql.kitsu.KitsuGetMangaDetailsByIdQuery +import mihon.graphql.kitsu.KitsuGetMangaDetailsBySlugQuery +import mihon.graphql.kitsu.KitsuSearchMangaByTitleQuery +import mihon.graphql.kitsu.KitsuUpdateLibMangaMutation import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.system.logcat import uy.kohesive.injekt.injectLazy @@ -42,301 +41,143 @@ class KitsuApi( private val authClient = client.newBuilder().addInterceptor(interceptor).build() + private val graphQlClient by lazy { + ApolloClient.Builder() + .serverUrl("https://kitsu.app/api/graphql") + .okHttpClient(authClient) + .dispatcher(Dispatchers.IO) + // required to log the error body in dataOrElse, which also properly closes it + .httpExposeErrorBody(true) + .build() + } + suspend fun addLibManga(track: Track): Track { - return withIOContext { - val query = $$""" - |mutation AddManga( - |$media_id: ID! - |$status: LibraryEntryStatusEnum! - |$progress: Int! - |$private: Boolean! - |$rating: Int - |) { - |libraryEntry { - |create( - |input: { - |mediaId: $media_id - |mediaType: MANGA - |status: $status - |progress: $progress - |private: $private - |rating: $rating - |} - |) { - |errors { - |message - |} - |libraryEntry { - |id - |} - |} - |} - |} - """.trimMargin() - - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("media_id", track.remote_id) - put("status", track.toKitsuApiStatus()) - put("progress", track.last_chapter_read.toInt()) - put("private", track.private) - put("rating", track.score.toInt().takeIf { it > 0 }) - } - } - - with(json) { - val parsed = authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - - if (parsed.error != null) { - logcat(LogPriority.ERROR) { "Failed to add: ${parsed.error.message ?: "(none)"}" } - throw Exception("Failed to add manga") - } else if (parsed.errors != null) { - parsed.errors.forEach { - logcat(LogPriority.ERROR) { "Failed to add: ${it.message ?: "(none)"}" } - } - throw Exception("Failed to add manga") - } else if (parsed.data == null) { - logcat(LogPriority.ERROR) { "Kitsu error, errors, and data null?" } - throw Exception("Encountered unexpected error while adding manga") - } - - parsed.data.libraryEntry.create.libraryEntry.id.let { - track.library_id = it.toLong() + return graphQlClient + .mutation( + KitsuAddLibMangaMutation( + media_id = track.remote_id.toString(), + status = track.toKitsuStatus(), + progress = track.last_chapter_read.toInt(), + private = track.private, + rating = Optional.present(track.score.toInt().takeIf { it > 0 }), + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Failed to add manga", + default = { null }, + ) { + it.libraryEntry.create?.libraryEntry?.id?.let { libraryId -> + track.library_id = libraryId.toLong() track } } - } + ?: throw Exception("Failed to add manga") } suspend fun updateLibManga(track: Track): Track { - return withIOContext { - val query = $$""" - |mutation UpdateManga( - |$library_id: ID! - |$status: LibraryEntryStatusEnum! - |$progress: Int! - |$private: Boolean! - |$rating: Int - |$startedAt: ISO8601DateTime - |$finishedAt: ISO8601DateTime - |) { - |libraryEntry { - |update( - |input: { - |id: $library_id - |status: $status - |progress: $progress - |private: $private - |rating: $rating - |startedAt: $startedAt - |finishedAt: $finishedAt - |} - |) { - |errors { - |message - |} - |libraryEntry { - |id - |} - |} - |} - |} - """.trimMargin() + val libraryId = track.library_id + requireNotNull(libraryId) { "Kitsu cannot update track with null library_id" } - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("library_id", track.library_id) - put("status", track.toKitsuApiStatus()) - put("progress", track.last_chapter_read.toInt()) - put("private", track.private) - put("rating", track.score.toInt().takeIf { it > 0 }) - put( - "startedAt", + return graphQlClient + .mutation( + KitsuUpdateLibMangaMutation( + library_id = libraryId.toString(), + status = track.toKitsuStatus(), + progress = track.last_chapter_read.toInt(), + private = track.private, + rating = Optional.present(track.score.toInt().takeIf { it > 0 }), + startedAt = Optional.present( track.started_reading_date .takeIf { it > 0 } - ?.let { Instant.fromEpochMilliseconds(it).toString() }, - ) - put( - "finishedAt", + ?.let { Instant.fromEpochMilliseconds(it) }, + ), + finishedAt = Optional.present( track.finished_reading_date .takeIf { it > 0 } - ?.let { Instant.fromEpochMilliseconds(it).toString() }, - ) - } - } - - with(json) { - val parsed = authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), + ?.let { Instant.fromEpochMilliseconds(it) }, ), - ) - .awaitSuccess() - .parseAs() - - if (parsed.error != null) { - logcat(LogPriority.ERROR) { "Failed to update: ${parsed.error.message ?: "(none)"}" } - throw Exception("Failed to update manga") - } else if (parsed.errors != null) { - parsed.errors.forEach { - logcat(LogPriority.ERROR) { "Failed to update: ${it.message ?: "(none)"}" } - } - throw Exception("Failed to update manga") - } else if (parsed.data == null) { - logcat(LogPriority.ERROR) { "Kitsu error, errors, and data null?" } - throw Exception("Encountered unexpected error while updating manga") + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Failed to update manga", + default = { null }, + ) { + it.libraryEntry.update?.libraryEntry?.id?.let { libraryId -> + logcat { "Kitsu: Updated library entry $libraryId" } + track.library_id = libraryId.toLong() + track } - - track } - } + ?: throw Exception("Failed to update manga") } suspend fun removeLibManga(track: DomainTrack) { - withIOContext { - val query = $$"""| - |mutation DeleteLibEntry( - |$library_id: ID! - |) { - |libraryEntry { - |delete( - |input: { - |id: $library_id - |} - |) { - |errors { - |message - |} - |libraryEntry { - |id - |} - |} - |} - |} - """.trimMargin() + val libraryId = track.libraryId + requireNotNull(libraryId) { "Kitsu cannot delete track with null library_id" } - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("library_id", track.libraryId) - } - } - - with(json) { - val parsed = authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - // Deleting something not in the library returns a 500 with "Couldn't find LibraryEntry" msg - // awaitSuccess would throw with that but user gets their wish of "title not in library" so ignore it - .await() - .parseAs() - - if (parsed.error != null) { - logcat(LogPriority.ERROR) { "Failed to delete: ${parsed.error.message ?: "(none)"}" } - if (parsed.error.message != null && parsed.error.message.startsWith("Couldn't find")) { - return@with + graphQlClient + .mutation( + KitsuDeleteLibEntryMutation( + library_id = libraryId.toString(), + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Failed to delete manga", + default = {}, + onException = { e -> + val body = e.body?.use { it.readUtf8() } + if ( + e.statusCode == 500 && + body?.contains("Couldn't find LibraryEntry with 'id'=") == true + ) { + // Deleting something not in the library (currently as of 2026-09-20) returns a 500 with a + // "Couldn't find LibraryEntry" error message at the error.message key -- + // but the user gets their wish of "title not in library" so ignore it + return@dataOrElse + } else { + throw HttpException(e.statusCode).apply { stackTrace = e.stackTrace } } - throw Exception("Failed to delete manga") - } else if (parsed.errors != null) { - parsed.errors.forEach { - logcat(LogPriority.ERROR) { "Failed to delete: ${it.message ?: "(none)"}" } - } - throw Exception("Failed to delete manga") - } else if (parsed.data == null) { - logcat(LogPriority.ERROR) { "Kitsu error, errors, and data null?" } - throw Exception("Encountered unexpected error while deleting manga") - } + }, + ) { + logcat { "Kitsu: Deleted library entry ${it.libraryEntry.delete?.libraryEntry?.id}" } } - } } suspend fun search(search: String): List { - return withIOContext { - val query = $$""" - |query Query($query: String!) { - |searchMangaByTitle(title: $query, first: 20) { - |nodes { - $$COMMON_MANGA_DATA - |} - |} - |} - """.trimMargin() - - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("query", search) - } + return graphQlClient + .query( + KitsuSearchMangaByTitleQuery( + query = search, + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Search failed", + default = { emptyList() }, + ) { + it.searchMangaByTitle.nodes + ?.mapNotNull { node -> node?.toTrackSearch(trackerId) } } - - with(json) { - authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.searchMangaByTitle.nodes - .map { it.toTrackSearch(trackerId) } - } - } + ?: emptyList() } suspend fun findLibManga(track: Track): Track? { - return withIOContext { - val query = $$""" - |query Query($remote_id: ID!) { - |findMangaById(id: $remote_id) { - |$$COMMON_MANGA_DATA - |myLibraryEntry { - |id - |private - |progress - |rating - |reconsuming - |status - |startedAt - |finishedAt - |} - |} - |} - """.trimMargin() - - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("remote_id", track.remote_id) - } + return graphQlClient + .query( + KitsuFindLibMangaQuery( + remote_id = track.remote_id.toString(), + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Failed to find manga in library", + default = { null }, + ) { + it.findMangaById?.toTrackSearch(trackerId) } - - with(json) { - authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.findMangaById - ?.toTrackSearch(trackerId) - } - } } suspend fun login(username: String, password: String): KitsuOAuth { @@ -356,96 +197,67 @@ class KitsuApi( } } - suspend fun getCurrentUser(): KitsuAccount { - return withIOContext { - val query = """ - |query Query { - |currentAccount { - |id - |ratingSystem - |profile { - |name - |} - |} - |} - """.trimMargin() - - val payload = buildJsonObject { - put("query", query) + suspend fun getCurrentUser(): KitsuUser { + return graphQlClient + .query( + KitsuGetCurrentAccountQuery(), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Failed to get current user", + default = { null }, + ) { + it.currentAccount?.toKitsuUser() } - - with(json) { - authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.currentAccount - } - } + ?: throw Exception("Failed to get Kitsu user data") } suspend fun getMangaDetails(search: String): TrackSearch? { val isSearchById = search.matches(Regex("\\d+")) - val query = if (isSearchById) { - $$""" - |query Query($query: ID!) { - |findMangaById(id: $query) { - |$$COMMON_MANGA_DATA - |} - |} - """ + return if (isSearchById) { + getMangaDetailsById(search) } else { - $$""" - |query Query($query: String!) { - |findMangaBySlug(slug: $query) { - |$$COMMON_MANGA_DATA - |} - |} - """ + getMangaDetailsBySlug(search) } + } - val payload = buildJsonObject { - put("query", query.trimMargin()) - putJsonObject("variables") { - put("query", search) + private suspend fun getMangaDetailsById(id: String): TrackSearch? { + return graphQlClient + .query( + KitsuGetMangaDetailsByIdQuery( + id = id, + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Search by ID failed", + default = { null }, + ) { + it.findMangaById?.toTrackSearch(trackerId) } - } + } - return withIOContext { - with(json) { - val response = authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - - val kitsuManga = if (isSearchById) { - response - .parseAs() - .data.findMangaById - } else { - response - .parseAs() - .data.findMangaBySlug - } - - kitsuManga?.toTrackSearch(trackerId) + private suspend fun getMangaDetailsBySlug(slug: String): TrackSearch? { + return graphQlClient + .query( + KitsuGetMangaDetailsBySlugQuery( + slug = slug, + ), + ) + .execute() + .dataOrElse( + errorLog = "Kitsu: Search by Slug failed", + default = { null }, + ) { + it.findMangaBySlug?.toTrackSearch(trackerId) } - } } companion object { private const val CLIENT_ID = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd" private const val CLIENT_SECRET = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151" - private const val GRAPHQL_API_URL = "https://kitsu.app/api/graphql" private const val LOGIN_URL = "https://kitsu.app/api/oauth/token" fun refreshTokenRequest(token: String) = POST( @@ -457,38 +269,5 @@ class KitsuApi( .add("client_secret", CLIENT_SECRET) .build(), ) - - private val COMMON_MANGA_DATA = """ - |id - |titles { - |preferred - |} - |chapterCount - |staff(first: 5) { - |nodes { - |role - |person { - |name - |} - |} - |} - |posterImage { - |views(names: "small") { - |name - |url - |} - |original { - |name - |url - |} - |} - |description(locales: "en") - |status - |subtype - |startDate - |endDate - |slug - |averageRating - """.trimMargin() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt index 401a193e6..20eb77930 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/KitsuUtils.kt @@ -1,21 +1,98 @@ package eu.kanade.tachiyomi.data.track.kitsu import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuUser +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import mihon.graphql.kitsu.KitsuFindLibMangaQuery +import mihon.graphql.kitsu.KitsuGetCurrentAccountQuery +import mihon.graphql.kitsu.KitsuGetMangaDetailsByIdQuery +import mihon.graphql.kitsu.KitsuGetMangaDetailsBySlugQuery +import mihon.graphql.kitsu.KitsuSearchMangaByTitleQuery +import mihon.graphql.kitsu.fragment.MangaFragment +import mihon.graphql.kitsu.type.LibraryEntryStatusEnum +import mihon.graphql.kitsu.type.MangaSubtypeEnum +import mihon.graphql.kitsu.type.ReleaseStatusEnum -internal fun Track.toKitsuApiStatus() = when (status) { - Kitsu.READING -> "CURRENT" - Kitsu.COMPLETED -> "COMPLETED" - Kitsu.ON_HOLD -> "ON_HOLD" - Kitsu.DROPPED -> "DROPPED" - Kitsu.PLAN_TO_READ -> "PLANNED" - else -> throw Exception("Unknown status: $status") +internal fun Track.toKitsuStatus() = when (this.status) { + Kitsu.READING -> LibraryEntryStatusEnum.CURRENT + Kitsu.PLAN_TO_READ -> LibraryEntryStatusEnum.PLANNED + Kitsu.COMPLETED -> LibraryEntryStatusEnum.COMPLETED + Kitsu.ON_HOLD -> LibraryEntryStatusEnum.ON_HOLD + Kitsu.DROPPED -> LibraryEntryStatusEnum.DROPPED + else -> throw Exception("Unknown status: ${this.status}") } -internal fun String.toKitsuLocalStatus() = when (this) { - "CURRENT" -> Kitsu.READING - "COMPLETED" -> Kitsu.COMPLETED - "ON_HOLD" -> Kitsu.ON_HOLD - "DROPPED" -> Kitsu.DROPPED - "PLANNED" -> Kitsu.PLAN_TO_READ +internal fun LibraryEntryStatusEnum.toLocalStatus(): Long = when (this) { + LibraryEntryStatusEnum.CURRENT -> Kitsu.READING + LibraryEntryStatusEnum.PLANNED -> Kitsu.PLAN_TO_READ + LibraryEntryStatusEnum.COMPLETED -> Kitsu.COMPLETED + LibraryEntryStatusEnum.ON_HOLD -> Kitsu.ON_HOLD + LibraryEntryStatusEnum.DROPPED -> Kitsu.DROPPED else -> throw Exception("Unknown status: $this") } + +internal fun KitsuGetCurrentAccountQuery.CurrentAccount.toKitsuUser(): KitsuUser { + return KitsuUser( + id = id, + name = profile.name, + ratingSystem = ratingSystem.rawValue, + ) +} + +internal fun KitsuGetMangaDetailsByIdQuery.FindMangaById.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +internal fun KitsuGetMangaDetailsBySlugQuery.FindMangaBySlug.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +internal fun KitsuSearchMangaByTitleQuery.Node.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +internal fun KitsuFindLibMangaQuery.FindMangaById.toTrackSearch(trackId: Long): TrackSearch? { + if (myLibraryEntry == null) return null + + return mangaFragment.toTrackSearch(trackId).apply { + library_id = myLibraryEntry.id.toLong() + started_reading_date = myLibraryEntry.startedAt?.toEpochMilliseconds() ?: 0 + finished_reading_date = myLibraryEntry.finishedAt?.toEpochMilliseconds() ?: 0 + status = myLibraryEntry.status.toLocalStatus() + score = myLibraryEntry.rating?.toDouble() ?: 0.0 + last_chapter_read = myLibraryEntry.progress.toDouble() + private = myLibraryEntry.private + } +} + +private fun MangaFragment.toTrackSearch(trackId: Long): TrackSearch { + val kitsuManga = this + return TrackSearch.create(trackId).apply { + remote_id = kitsuManga.id.toLong() + title = kitsuManga.titles.preferred + total_chapters = kitsuManga.chapterCount?.toLong() ?: 0 + cover_url = kitsuManga.posterImage?.views?.firstOrNull()?.url ?: kitsuManga.posterImage?.original?.url.orEmpty() + summary = (kitsuManga.description["en"] as? String).orEmpty() + tracking_url = "https://kitsu.app/manga/${kitsuManga.slug}" + score = kitsuManga.averageRating ?: -1.0 + publishing_status = when (kitsuManga.status) { + ReleaseStatusEnum.TBA -> ReleaseStatusEnum.TBA.rawValue + else -> kitsuManga.status.rawValue.lowercase().replaceFirstChar { it.uppercase() } + } + publishing_type = when (kitsuManga.subtype) { + MangaSubtypeEnum.OEL -> MangaSubtypeEnum.OEL.rawValue + else -> kitsuManga.subtype.rawValue.lowercase().replaceFirstChar { it.uppercase() } + } + start_date = kitsuManga.startDate.orEmpty() + authors = kitsuManga.staff.nodes + ?.filterNotNull() + ?.filter { it.role.contains("Story") } + ?.map { it.person.name } + ?: emptyList() + artists = kitsuManga.staff.nodes + ?.filterNotNull() + ?.filter { it.role.contains("Art") } + ?.map { it.person.name } + ?: emptyList() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt deleted file mode 100644 index 35d1b225a..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuAddManga.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuAddMangaResult( - // yes there are two different error attributes and yes they have different structures - // it seems both are valid in different cases - // (500s send "error" for example, 200s with validation errors send "errors") - val data: KitsuAddMangaData?, - val errors: List?, - val error: KitsuErrorMessage?, -) - -@Serializable -data class KitsuAddMangaData( - val libraryEntry: KitsuAddMangaLibraryEntryWrapper, -) - -@Serializable -data class KitsuAddMangaLibraryEntryWrapper( - val create: KitsuLibraryEntryResult, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuCommon.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuCommon.kt deleted file mode 100644 index 42d7a3083..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuCommon.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuErrorMessage( - val message: String?, -) - -@Serializable -data class KitsuLibraryEntryResult( - val libraryEntry: KitsuSparseLibraryEntry, -) - -@Serializable -data class KitsuSparseLibraryEntry( - val id: String, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuDeleteManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuDeleteManga.kt deleted file mode 100644 index 23b3090a9..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuDeleteManga.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuDeleteMangaResult( - // yes there are two different error attributes and yes they have different structures - // it seems both are valid in different cases - // (500s send "error" for example, 200s with validation errors send "errors") - val data: KitsuDeleteMangaData?, - val errors: List?, - val error: KitsuErrorMessage?, -) - -@Serializable -data class KitsuDeleteMangaData( - val libraryEntry: KitsuDeleteMangaLibraryEntryWrapper, -) - -@Serializable -data class KitsuDeleteMangaLibraryEntryWrapper( - val delete: KitsuLibraryEntryResult, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuManga.kt deleted file mode 100644 index c9deb0ff1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuManga.kt +++ /dev/null @@ -1,85 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuManga( - val id: String, - val titles: KitsuMangaTitles, - val chapterCount: Long?, - val staff: KitsuMangaStaffData, - val posterImage: KitsuMangaPosters, - val description: Map, - val status: String, - val subtype: String, - val startDate: String?, - val endDate: String?, - val slug: String, - val averageRating: Double?, -) { - fun toTrackSearch(trackId: Long): TrackSearch { - return TrackSearch.create(trackId).apply { - remote_id = this@KitsuManga.id.toLong() - title = titles.preferred - total_chapters = chapterCount ?: 0 - cover_url = posterImage.getPosterUrl() - summary = description["en"] ?: "" - tracking_url = "https://kitsu.app/manga/$slug" - score = averageRating ?: -1.0 - publishing_status = when (this@KitsuManga.status) { - "TBA" -> "TBA" - "CURRENT" -> "Publishing" - else -> this@KitsuManga.status.lowercase().replaceFirstChar { it.uppercase() } - } - publishing_type = if (subtype != "OEL") { - subtype.lowercase().replaceFirstChar { it.uppercase() } - } else { - subtype - } - start_date = startDate ?: "" - authors = staff.nodes - .filter { it.role.contains("Story") } - .map { it.person.name } - artists = staff.nodes - .filter { it.role.contains("Art") } - .map { it.person.name } - } - } -} - -@Serializable -data class KitsuMangaTitles( - val preferred: String, -) - -@Serializable -data class KitsuMangaStaffData( - val nodes: List, -) - -@Serializable -data class KitsuMangaStaff( - val role: String, - val person: KitsuMangaStaffPerson, -) - -@Serializable -data class KitsuMangaStaffPerson( - val name: String, -) - -@Serializable -data class KitsuMangaPosters( - val views: List, - val original: KitsuMangaPoster, -) { - // we only ask for the "small" poster in the query - fun getPosterUrl(): String = views.firstOrNull()?.url ?: original.url -} - -@Serializable -data class KitsuMangaPoster( - val name: String, - val url: String, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaWithLibraryEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaWithLibraryEntry.kt deleted file mode 100644 index 3f50ab4f1..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuMangaWithLibraryEntry.kt +++ /dev/null @@ -1,74 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import eu.kanade.tachiyomi.data.track.kitsu.toKitsuLocalStatus -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import kotlinx.serialization.Serializable -import kotlin.time.Instant - -// KitsuManga extended with KitsuLibraryEntryData -@Serializable -data class KitsuMangaWithLibraryEntry( - val id: String, - val titles: KitsuMangaTitles, - val chapterCount: Long?, - val staff: KitsuMangaStaffData, - val posterImage: KitsuMangaPosters, - val description: Map, - val status: String, - val subtype: String, - val startDate: String?, - val endDate: String?, - val slug: String, - val averageRating: Double?, - val myLibraryEntry: KitsuLibraryEntryData?, -) { - fun toTrackSearch(trackId: Long): TrackSearch? { - if (myLibraryEntry == null) return null - - return TrackSearch.create(trackId).apply { - remote_id = this@KitsuMangaWithLibraryEntry.id.toLong() - library_id = myLibraryEntry.id.toLong() - title = titles.preferred - total_chapters = chapterCount ?: 0 - cover_url = posterImage.getPosterUrl() - summary = description["en"] ?: "" - tracking_url = "https://kitsu.app/manga/$slug" - publishing_status = when (this@KitsuMangaWithLibraryEntry.status) { - "TBA" -> "TBA" - "CURRENT" -> "Publishing" - else -> this@KitsuMangaWithLibraryEntry.status.lowercase().replaceFirstChar { it.uppercase() } - } - publishing_type = if (subtype != "OEL") { - subtype.lowercase().replaceFirstChar { it.uppercase() } - } else { - subtype - } - start_date = startDate ?: "" - authors = staff.nodes - .filter { it.role.contains("Story") } - .map { it.person.name } - artists = staff.nodes - .filter { it.role.contains("Art") } - .map { it.person.name } - - started_reading_date = myLibraryEntry.startedAt?.let { Instant.parse(it).toEpochMilliseconds() } ?: 0 - finished_reading_date = myLibraryEntry.finishedAt?.let { Instant.parse(it).toEpochMilliseconds() } ?: 0 - status = myLibraryEntry.status.toKitsuLocalStatus() - score = myLibraryEntry.rating?.toDouble() ?: 0.0 - last_chapter_read = myLibraryEntry.progress.toDouble() - private = myLibraryEntry.private - } - } -} - -@Serializable -data class KitsuLibraryEntryData( - val id: String, - val private: Boolean, - val progress: Long, - val rating: Long?, - val reconsuming: Boolean, - val status: String, - val startedAt: String?, - val finishedAt: String?, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchById.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchById.kt deleted file mode 100644 index bf98d5ec8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchById.kt +++ /dev/null @@ -1,25 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -// normal search -@Serializable -data class KitsuSearchByIdResult( - val data: KitsuSearchByIdData, -) - -@Serializable -data class KitsuSearchByIdData( - val findMangaById: KitsuManga?, -) - -// findLibManga (on tracker sheet refresh & when checking for remote track on binding) -@Serializable -data class KitsuSearchByIdWithLibraryResult( - val data: KitsuSearchByIdWithLibraryData, -) - -@Serializable -data class KitsuSearchByIdWithLibraryData( - val findMangaById: KitsuMangaWithLibraryEntry?, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchBySlug.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchBySlug.kt deleted file mode 100644 index 532ec754d..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchBySlug.kt +++ /dev/null @@ -1,13 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuSearchBySlugResult( - val data: KitsuSearchBySlugData, -) - -@Serializable -data class KitsuSearchBySlugData( - val findMangaBySlug: KitsuManga?, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchByTitle.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchByTitle.kt deleted file mode 100644 index 6393eaf73..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuSearchByTitle.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuSearchByTitleResult( - val data: KitsuSearchByTitleData, -) - -@Serializable -data class KitsuSearchByTitleData( - val searchMangaByTitle: KitsuSearchNodes, -) - -@Serializable -data class KitsuSearchNodes( - val nodes: List, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUpdateManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUpdateManga.kt deleted file mode 100644 index 78344ab1b..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUpdateManga.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.kanade.tachiyomi.data.track.kitsu.dto - -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuUpdateMangaResult( - // yes there are two different error attributes and yes they have different structures - // it seems both are valid in different cases - // (500s send "error" for example, 200s with validation errors send "errors") - val data: KitsuUpdateMangaData?, - val errors: List?, - val error: KitsuErrorMessage?, -) - -@Serializable -data class KitsuUpdateMangaData( - val libraryEntry: KitsuUpdateMangaLibraryEntryWrapper, -) - -@Serializable -data class KitsuUpdateMangaLibraryEntryWrapper( - val update: KitsuLibraryEntryResult, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt index 86ec9dbbb..b8650559f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/kitsu/dto/KitsuUser.kt @@ -1,25 +1,7 @@ package eu.kanade.tachiyomi.data.track.kitsu.dto -import kotlinx.serialization.Serializable - -@Serializable -data class KitsuCurrentAccountResult( - val data: KitsuCurrentAccountData, -) - -@Serializable -data class KitsuCurrentAccountData( - val currentAccount: KitsuAccount, -) - -@Serializable -data class KitsuAccount( +data class KitsuUser( val id: String, - val ratingSystem: String, - val profile: KitsuProfile, -) - -@Serializable -data class KitsuProfile( val name: String, + val ratingSystem: String, ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt index 7ab177c66..bc02fa039 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriApi.kt @@ -2,24 +2,29 @@ package eu.kanade.tachiyomi.data.track.shikimori import android.net.Uri import androidx.core.net.toUri +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.network.okHttpClient import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.shikimori.dto.SMLibraryIdResponse import eu.kanade.tachiyomi.data.track.shikimori.dto.SMOAuth -import eu.kanade.tachiyomi.data.track.shikimori.dto.SMSearchResult import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUser -import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserListResult -import eu.kanade.tachiyomi.data.track.shikimori.dto.SMUserResult import eu.kanade.tachiyomi.network.DELETE import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.PUT import eu.kanade.tachiyomi.network.awaitSuccess +import eu.kanade.tachiyomi.network.dataOrElse import eu.kanade.tachiyomi.network.jsonMime import eu.kanade.tachiyomi.network.parseAs +import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.Json import kotlinx.serialization.json.buildJsonObject import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonObject +import mihon.graphql.shikimori.ShikimoriGetCurrentUserQuery +import mihon.graphql.shikimori.ShikimoriGetLibMangaQuery +import mihon.graphql.shikimori.ShikimoriGetMangaDetailsQuery +import mihon.graphql.shikimori.ShikimoriSearchMangaQuery import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.RequestBody.Companion.toRequestBody @@ -37,6 +42,16 @@ class ShikimoriApi( private val authClient = client.newBuilder().addInterceptor(interceptor).build() + private val graphQlClient by lazy { + ApolloClient.Builder() + .serverUrl("$API_URL/graphql") + .okHttpClient(authClient) + .dispatcher(Dispatchers.IO) + // required to log the error body in dataOrElse, which also properly closes it + .httpExposeErrorBody(true) + .build() + } + suspend fun addLibManga(track: Track, userId: String): Track { return withIOContext { with(json) { @@ -102,178 +117,78 @@ class ShikimoriApi( } suspend fun search(search: String): List { - return withIOContext { - val query = $$""" - |query($query: String) { - |mangas(search: $query, limit: 20, kind:"!light_novel,!novel") { - |id - |name - |chapters - |kind - |poster { - |mainUrl - |} - |score - |url - |status - |airedOn { - |date - |} - |description - |personRoles { - |person { - |name - |} - |rolesEn - |} - |} - |} - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("query", search) + return graphQlClient + .query( + ShikimoriSearchMangaQuery(search = search), + ) + .execute() + .dataOrElse( + errorLog = "Shikimori: Search failed", + default = { emptyList() }, + ) { + it.mangas.map { manga -> + manga.toTrackSearch(trackerId) } } - with(json) { - authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.mangas - .map { it.toTrack(trackerId) } - } - } } suspend fun getMangaDetails(id: Int): TrackSearch? { - return withIOContext { - val query = $$""" - |query($query: String) { - |mangas(ids: $query, limit: 1, kind:"!light_novel,!novel") { - |id - |name - |chapters - |kind - |poster { - |mainUrl - |} - |score - |url - |status - |airedOn { - |date - |} - |description - |personRoles { - |person { - |name - |} - |rolesEn - |} - |} - |} - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("query", "$id") - } - } - - with(json) { - authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.mangas + return graphQlClient + .query( + ShikimoriGetMangaDetailsQuery(query = "$id"), + ) + .execute() + .dataOrElse( + errorLog = "Shikimori: Failed to get manga details", + default = { null }, + ) { + it.mangas .firstOrNull() - ?.toTrack(trackerId) + ?.toTrackSearch(trackerId) } - } } suspend fun findLibManga(track: Track): Track? { - return withIOContext { - val query = $$""" - |query($id: String) { - |mangas(ids: $id, limit: 1) { - |id - |url - |name - |chapters - |userRate { - |id - |chapters - |status - |score - |} - |} - |} - """.trimMargin() - - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("id", track.remote_id.toString()) - } - } - with(json) { - val listResult = authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - .parseAs() - .data.mangas - .firstOrNull() + return graphQlClient + .query( + ShikimoriGetLibMangaQuery( + remote_id = track.remote_id.toString(), + ), + ) + .execute() + .dataOrElse( + errorLog = "Shikimori: Failed to find manga in library", + default = { null }, + ) { + val mangaResult = it.mangas.firstOrNull() // Shikimori has no user list query that allows query by ID, so we go via the "mangas" query & include // userRate data which will be null if the title is not in the user's list. // If it was removed on Shikimori and is still linked in the app, notify user via returning null here // which throws an exception at the Shikimori.refresh call - if (listResult?.userRate == null) { + if (mangaResult?.userRate == null) { null } else { - listResult.toTrack(trackerId) + mangaResult.toTrack(trackerId) } } - } } suspend fun getCurrentUser(): SMUser { - return with(json) { - val query = """ - |{ - |currentUser { - |id - |nickname - |} - |} - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - } - authClient.newCall( - POST( - GRAPHQL_API_URL, - body = payload.toString().toRequestBody(jsonMime), - ), + return graphQlClient + .query( + ShikimoriGetCurrentUserQuery(), ) - .awaitSuccess() - .parseAs() - .data.currentUser - } + .execute() + .dataOrElse( + errorLog = "Shikimori: Failed to get current user", + default = { null }, + ) { + it.currentUser?.let { currentUser -> + SMUser(id = currentUser.id, nickname = currentUser.nickname) + } + } + ?: throw Exception("Failed to get Shikimori user data") } suspend fun accessToken(code: String): SMOAuth { @@ -300,7 +215,6 @@ class ShikimoriApi( companion object { private const val BASE_URL = "https://shikimori.io" private const val API_URL = "$BASE_URL/api" - private const val GRAPHQL_API_URL = "$BASE_URL/api/graphql" private const val OAUTH_URL = "$BASE_URL/oauth/token" private const val LOGIN_URL = "$BASE_URL/oauth/authorize" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt index 234af7587..9bf71dc50 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/ShikimoriUtils.kt @@ -1,23 +1,97 @@ package eu.kanade.tachiyomi.data.track.shikimori import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import mihon.graphql.shikimori.ShikimoriGetLibMangaQuery +import mihon.graphql.shikimori.ShikimoriGetMangaDetailsQuery +import mihon.graphql.shikimori.ShikimoriSearchMangaQuery +import mihon.graphql.shikimori.fragment.MangaFragment +import mihon.graphql.shikimori.type.MangaKindEnum +import mihon.graphql.shikimori.type.MangaStatusEnum +import mihon.graphql.shikimori.type.UserRateStatusEnum + +internal fun ShikimoriGetLibMangaQuery.Manga.toTrack(trackId: Long): Track { + val sManga = this + return Track.create(trackId).apply { + remote_id = sManga.id.toLong() + title = sManga.name + total_chapters = sManga.chapters.toLong() + tracking_url = sManga.url + + if (sManga.userRate != null) { + // null if not in user's list, must not throw here because it'd break adding titles + // throws in the findLibManga method of ShikimoriApi if null and shouldn't be + library_id = sManga.userRate.id.toLong() + last_chapter_read = sManga.userRate.chapters.toDouble() + score = sManga.userRate.score.toDouble() + status = sManga.userRate.status.toLocalStatus() + } + } +} + +internal fun ShikimoriGetMangaDetailsQuery.Manga.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +internal fun ShikimoriSearchMangaQuery.Manga.toTrackSearch(trackId: Long): TrackSearch { + return mangaFragment.toTrackSearch(trackId) +} + +private fun MangaFragment.toTrackSearch(trackId: Long): TrackSearch { + val sManga = this + return TrackSearch.create(trackId).apply { + remote_id = sManga.id.toLong() + title = sManga.name + total_chapters = sManga.chapters.toLong() + cover_url = sManga.poster?.mainUrl.orEmpty() + summary = sManga.description.orEmpty() + score = sManga.score?.takeIf { it > 0.0 } ?: -1.0 + tracking_url = sManga.url + publishing_status = sManga.status.toLocalString() + publishing_type = sManga.kind.toLocalString() + start_date = sManga.airedOn?.date.orEmpty() + authors = sManga.personRoles?.filter { it.rolesEn.contains("Story") }?.map { it.person.name } ?: emptyList() + artists = sManga.personRoles?.filter { it.rolesEn.contains("Art") }?.map { it.person.name } ?: emptyList() + } +} + +private fun MangaKindEnum?.toLocalString() = when (this) { + MangaKindEnum.manga -> "Manga" + MangaKindEnum.manhwa -> "Manhwa" + MangaKindEnum.manhua -> "Manhua" + MangaKindEnum.one_shot -> "Oneshot" + MangaKindEnum.doujin -> "Doujin" + // light novel & novel omitted by query filter, fall back to empty if included + else -> "" +} + +private fun MangaStatusEnum?.toLocalString() = when (this) { + MangaStatusEnum.anons -> "Planned" + MangaStatusEnum.ongoing -> "Ongoing" + MangaStatusEnum.released -> "Released" + MangaStatusEnum.paused -> "Paused" + MangaStatusEnum.discontinued -> "Discontinued" + else -> "" +} + +private fun UserRateStatusEnum.toLocalStatus() = when (this) { + UserRateStatusEnum.planned -> Shikimori.PLAN_TO_READ + UserRateStatusEnum.watching -> Shikimori.READING + UserRateStatusEnum.rewatching -> Shikimori.REREADING + UserRateStatusEnum.completed -> Shikimori.COMPLETED + UserRateStatusEnum.on_hold -> Shikimori.ON_HOLD + UserRateStatusEnum.dropped -> Shikimori.DROPPED + else -> throw NotImplementedError("Unknown status: $this") +} internal fun Track.toShikimoriStatus() = when (status) { - Shikimori.READING -> "watching" - Shikimori.COMPLETED -> "completed" - Shikimori.ON_HOLD -> "on_hold" - Shikimori.DROPPED -> "dropped" - Shikimori.PLAN_TO_READ -> "planned" - Shikimori.REREADING -> "rewatching" - else -> throw NotImplementedError("Unknown status: $status") -} - -internal fun toTrackStatus(status: String) = when (status) { - "watching" -> Shikimori.READING - "completed" -> Shikimori.COMPLETED - "on_hold" -> Shikimori.ON_HOLD - "dropped" -> Shikimori.DROPPED - "planned" -> Shikimori.PLAN_TO_READ - "rewatching" -> Shikimori.REREADING + // rawValues because Shikimori doesn't have GraphQL mutations and this goes through the v2 API + // if/when Shikimori adds GraphQL mutations, this should return UserRateStatusEnum members + Shikimori.READING -> UserRateStatusEnum.watching.rawValue + Shikimori.COMPLETED -> UserRateStatusEnum.completed.rawValue + Shikimori.ON_HOLD -> UserRateStatusEnum.on_hold.rawValue + Shikimori.DROPPED -> UserRateStatusEnum.dropped.rawValue + Shikimori.PLAN_TO_READ -> UserRateStatusEnum.planned.rawValue + Shikimori.REREADING -> UserRateStatusEnum.rewatching.rawValue else -> throw NotImplementedError("Unknown status: $status") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt deleted file mode 100644 index ddd4e7d37..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMManga.kt +++ /dev/null @@ -1,73 +0,0 @@ -package eu.kanade.tachiyomi.data.track.shikimori.dto - -import eu.kanade.tachiyomi.data.track.model.TrackSearch -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class SMSearchResult( - val data: SMMangaResults, -) - -@Serializable -data class SMMangaResults( - val mangas: List, -) - -@Serializable -data class SMManga( - val id: Long, - val name: String, - val chapters: Long, - val score: Double?, - val url: String, - val status: String?, - val poster: SMPoster?, - val airedOn: SMAiredDate?, - val description: String?, - val kind: String?, - val personRoles: List?, -) { - fun toTrack(trackerId: Long): TrackSearch { - return TrackSearch.create(trackerId).apply { - remote_id = this@SMManga.id - title = name - total_chapters = chapters - cover_url = poster?.mainUrl.orEmpty() - summary = description.orEmpty() - score = this@SMManga.score?.takeIf { it > 0.0 } ?: -1.0 - tracking_url = url - publishing_status = this@SMManga.status.orEmpty() - publishing_type = kind?.replace("one_shot", "oneshot").orEmpty() - start_date = airedOn?.date.orEmpty() - personRoles?.forEach { personRole -> - personRole.roles.forEach { role -> - if ("Story" in role) authors += personRole.person.name - if ("Art" in role) artists += personRole.person.name - } - } - } - } -} - -@Serializable -data class SMPoster( - val mainUrl: String, -) - -@Serializable -data class SMAiredDate( - val date: String?, -) - -@Serializable -data class SMPersonRole( - val person: SMPerson, - @SerialName("rolesEn") - val roles: List, -) - -@Serializable -data class SMPerson( - val name: String, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt index 98a2c0db2..9203b4565 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUser.kt @@ -1,18 +1,5 @@ package eu.kanade.tachiyomi.data.track.shikimori.dto -import kotlinx.serialization.Serializable - -@Serializable -data class SMUserResult( - val data: SMCurrentUser, -) - -@Serializable -data class SMCurrentUser( - val currentUser: SMUser, -) - -@Serializable data class SMUser( val id: String, val nickname: String, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt deleted file mode 100644 index a6b3e36cd..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/shikimori/dto/SMUserListEntry.kt +++ /dev/null @@ -1,52 +0,0 @@ -package eu.kanade.tachiyomi.data.track.shikimori.dto - -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.track.shikimori.toTrackStatus -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class SMUserListResult( - val data: SMUserListEntries, -) - -@Serializable -data class SMUserListEntries( - val mangas: List, -) - -@Serializable -data class SMUserListManga( - val id: String, - val url: String, - val name: String, - @SerialName("chapters") - val totalChapters: Long, // the title's total chapters - val userRate: SMUserRate?, -) { - fun toTrack(trackerId: Long): Track { - return Track.create(trackerId).apply { - title = name - total_chapters = totalChapters - tracking_url = url - if (userRate != null) { - // null if not in user's list, must not throw here because it'd break adding titles - // throws in the findLibManga method of ShikimoriApi if null and shouldn't be - remote_id = userRate.rateId.toLong() - library_id = userRate.rateId.toLong() - last_chapter_read = userRate.chapters.toDouble() - score = userRate.score - status = toTrackStatus(userRate.status) - } - } - } -} - -@Serializable -data class SMUserRate( - @SerialName("id") - val rateId: String, // ID of the list entry (NOT the title) - val chapters: Long, // the user's chapter progress - val status: String, - val score: Double, -) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt index e7c62e7f5..64c5c75ee 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/Suwayomi.kt @@ -56,7 +56,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { } } - return api.updateProgress(track, getPrefTrackerDelete()) + return api.updateProgress(track, getPrefTrackerDelete()) ?: throw Exception("Could not update manga") } override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { @@ -68,7 +68,7 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { } override suspend fun refresh(track: Track): Track { - val remoteTrack = api.getTrackSearch(track.remote_id) + val remoteTrack = api.getTrackSearch(track.remote_id) ?: throw Exception("Could not find manga") track.copyPersonalFrom(remoteTrack) track.total_chapters = remoteTrack.total_chapters return track @@ -84,25 +84,27 @@ class Suwayomi(id: Long) : BaseTracker(id, "Suwayomi"), EnhancedTracker { override fun getAcceptedSources(): List = listOf("eu.kanade.tachiyomi.extension.all.tachidesk.Tachidesk") - override suspend fun match(manga: DomainManga): TrackSearch? = - try { + override suspend fun match(manga: DomainManga): TrackSearch? { + return try { api.getTrackSearch(manga.url.getMangaId()) } catch (_: Exception) { null } + } - override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean = - track.remoteUrl == manga.url && source?.let { accept(it) } == true + override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean { + return track.remoteUrl == manga.url && source?.let { accept(it) } == true + } - override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? = - if (accept(newSource)) { + override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? { + return if (accept(newSource)) { track.copy(remoteUrl = manga.url) } else { null } + } - private fun String.getMangaId(): Long = - this.substringAfterLast('/').toLong() + private fun String.getMangaId(): Long = this.substringAfterLast('/').toLong() private fun getPrefTrackerDelete(): Boolean { val preferences = api.sourcePreferences() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt index e3280b14c..c164c775f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiApi.kt @@ -1,187 +1,112 @@ package eu.kanade.tachiyomi.data.track.suwayomi import android.content.SharedPreferences +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.network.okHttpClient import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.network.POST -import eu.kanade.tachiyomi.network.awaitSuccess -import eu.kanade.tachiyomi.network.jsonMime -import eu.kanade.tachiyomi.network.parseAs +import eu.kanade.tachiyomi.network.dataOrElse import eu.kanade.tachiyomi.source.ConfigurableSource +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.sourcePreferences +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.addAll -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray -import kotlinx.serialization.json.putJsonObject +import mihon.graphql.suwayomi.SuwayomiGetMangaQuery +import mihon.graphql.suwayomi.SuwayomiGetMangaUnreadChaptersQuery +import mihon.graphql.suwayomi.SuwayomiMarkAndDeleteChaptersMutation +import mihon.graphql.suwayomi.SuwayomiMarkChaptersReadMutation +import mihon.graphql.suwayomi.SuwayomiUpdateMangaProgressMutation import okhttp3.OkHttpClient -import okhttp3.RequestBody.Companion.toRequestBody -import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.domain.source.service.SourceManager -import uy.kohesive.injekt.injectLazy import java.security.MessageDigest class SuwayomiApi( private val trackerId: Long, private val sourceManager: SourceManager, ) { - - private val json: Json by injectLazy() - // Blocking is fine here: these are only touched from OkHttp and tracker threads. - private val source: HttpSource by lazy { runBlocking { sourceManager.get(sourceId) as HttpSource } } - private val configurableSource: ConfigurableSource by lazy { - runBlocking { sourceManager.get(sourceId) as ConfigurableSource } - } - private val client: OkHttpClient by lazy { source.client } - private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') } + private val source: Source by lazy { runBlocking { sourceManager.get(sourceId)!! } } + private val httpSource: HttpSource by lazy { source as HttpSource } + private val configurableSource: ConfigurableSource by lazy { source as ConfigurableSource } + private val client: OkHttpClient by lazy { httpSource.client } + private val baseUrl: String by lazy { httpSource.baseUrl.trimEnd('/') } private val apiUrl: String by lazy { "$baseUrl/api/graphql" } + private val graphQlClient by lazy { + ApolloClient.Builder() + .serverUrl(apiUrl) + .okHttpClient(client) + .dispatcher(Dispatchers.IO) + // required to log the error body in dataOrElse, which also properly closes it + .httpExposeErrorBody(true) + .build() + } + fun sourcePreferences(): SharedPreferences = configurableSource.sourcePreferences() - suspend fun getTrackSearch(mangaId: Long): TrackSearch = withIOContext { - val query = $$""" - |query GetManga($mangaId: Int!) { - | manga(id: $mangaId) { - | ...MangaFragment - | } - |} - | - |$$MangaFragment - """.trimMargin() - val payload = buildJsonObject { - put("query", query) - putJsonObject("variables") { - put("mangaId", mangaId) - } - } - val manga = with(json) { - client.newCall( - POST( - apiUrl, - body = payload.toString().toRequestBody(jsonMime), - ), + suspend fun getTrackSearch(mangaId: Long): TrackSearch? { + return graphQlClient + .query( + SuwayomiGetMangaQuery(mangaId = mangaId.toInt()), ) - .awaitSuccess() - .parseAs() - .data - .entry - } - - TrackSearch.create(trackerId).apply { - remote_id = mangaId - title = manga.title - cover_url = "$baseUrl/${manga.thumbnailUrl}" - summary = manga.description.orEmpty() - tracking_url = "$baseUrl/manga/$mangaId" - total_chapters = manga.chapters.totalCount.toLong() - publishing_status = manga.status.name - last_chapter_read = manga.latestReadChapter?.chapterNumber ?: 0.0 - status = when (manga.unreadCount) { - manga.chapters.totalCount -> Suwayomi.UNREAD - 0 -> Suwayomi.COMPLETED - else -> Suwayomi.READING + .execute() + .dataOrElse( + errorLog = "Suwayomi: Failed to find manga in library", + default = { null }, + ) { + it.manga.mangaFragment.toTrackSearch(trackerId, baseUrl) } - } } - suspend fun updateProgress(track: Track, deleteDownloadsOnServer: Boolean = false): Track { + suspend fun updateProgress(track: Track, deleteDownloadsOnServer: Boolean = false): Track? { val mangaId = track.remote_id - // TODO: Include a filter on the chapter number here - // Below, we only consider older chapters; since v2.1.1985 filtering works properly in the query - val chaptersQuery = $$""" - |query GetMangaUnreadChapters($mangaId: Int!) { - | chapters(condition: {mangaId: $mangaId, isRead: false}) { - | nodes { - | id - | chapterNumber - | } - | } - |} - """.trimMargin() - val chaptersPayload = buildJsonObject { - put("query", chaptersQuery) - putJsonObject("variables") { - put("mangaId", mangaId) - } - } - val chaptersToMark = with(json) { - client.newCall( - POST( - apiUrl, - body = chaptersPayload.toString().toRequestBody(jsonMime), + val chaptersToMark = graphQlClient + .query( + SuwayomiGetMangaUnreadChaptersQuery( + mangaId = mangaId.toInt(), + chapterNumber = track.last_chapter_read, ), ) - .awaitSuccess() - .parseAs() - .data - .entry - .nodes - .mapNotNull { n -> n.id.takeIf { n.chapterNumber <= track.last_chapter_read + 0.001 } } + .execute() + .dataOrElse( + errorLog = "Suwayomi: Failed to get chapters data", + default = { null }, + ) { + it.chapters.nodes.map { chapter -> chapter.id } + } + ?: throw Exception("Could not get chapters data") + + if (chaptersToMark.isEmpty()) { + return getTrackSearch(mangaId) } - val markQuery = if (deleteDownloadsOnServer) { - $$""" - |mutation MarkChaptersRead($chapters: [Int!]!) { - | updateChapters(input: {ids: $chapters, patch: {isRead: true}}) { - | __typename - | } - | deleteDownloadedChapters(input: {ids: $chapters}) { - | __typename - | } - |} - """.trimMargin() + val markMutation = if (deleteDownloadsOnServer) { + SuwayomiMarkAndDeleteChaptersMutation(chapters = chaptersToMark) } else { - $$""" - |mutation MarkChaptersRead($chapters: [Int!]!) { - | updateChapters(input: {ids: $chapters, patch: {isRead: true}}) { - | __typename - | } - |} - """.trimMargin() + SuwayomiMarkChaptersReadMutation(chapters = chaptersToMark) } - val markPayload = buildJsonObject { - put("query", markQuery) - putJsonObject("variables") { - putJsonArray("chapters") { - addAll(chaptersToMark) - } - } - } - client.newCall( - POST( - apiUrl, - body = markPayload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() - val trackQuery = $$""" - |mutation TrackManga($mangaId: Int!) { - | trackProgress(input: {mangaId: $mangaId}) { - | __typename - | } - |} - """.trimMargin() - val trackPayload = buildJsonObject { - put("query", trackQuery) - putJsonObject("variables") { - put("mangaId", mangaId) - } - } - client.newCall( - POST( - apiUrl, - body = trackPayload.toString().toRequestBody(jsonMime), - ), - ) - .awaitSuccess() + graphQlClient + .mutation(markMutation) + .execute() + .dataOrElse( + errorLog = "Suwayomi: Failed to mark chapters", + default = {}, + ) {} - return getTrackSearch(track.remote_id) + graphQlClient + .mutation( + SuwayomiUpdateMangaProgressMutation(mangaId = mangaId.toInt()), + ) + .execute() + .dataOrElse( + errorLog = "Suwayomi: Failed to update progress", + default = {}, + ) {} + + return getTrackSearch(mangaId) } private val sourceId by lazy { @@ -189,36 +114,4 @@ class SuwayomiApi( val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE } - - companion object { - private val MangaFragment = """ - |fragment MangaFragment on MangaType { - | artist - | author - | description - | id - | status - | thumbnailUrl - | title - | url - | genre - | inLibraryAt - | chapters { - | totalCount - | } - | latestUploadedChapter { - | uploadDate - | } - | latestFetchedChapter { - | fetchedAt - | } - | latestReadChapter { - | lastReadAt - | chapterNumber - | } - | unreadCount - | downloadCount - |} - """.trimMargin() - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiMangaFragmentExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiMangaFragmentExtensions.kt new file mode 100644 index 000000000..a14c109e6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiMangaFragmentExtensions.kt @@ -0,0 +1,23 @@ +package eu.kanade.tachiyomi.data.track.suwayomi + +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import mihon.graphql.suwayomi.fragment.MangaFragment + +internal fun MangaFragment.toTrackSearch(trackId: Long, baseUrl: String): TrackSearch { + val manga = this + return TrackSearch.create(trackId).apply { + remote_id = manga.id.toLong() + title = manga.title + cover_url = "$baseUrl/${manga.thumbnailUrl}" + summary = manga.description.orEmpty() + tracking_url = "$baseUrl/manga/${manga.id}" + total_chapters = manga.chapters.totalCount.toLong() + publishing_status = manga.status.name + last_chapter_read = manga.latestReadChapter?.chapterNumber ?: 0.0 + status = when (manga.unreadCount) { + manga.chapters.totalCount -> Suwayomi.UNREAD + 0 -> Suwayomi.COMPLETED + else -> Suwayomi.READING + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt deleted file mode 100644 index 7fbd33505..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/suwayomi/SuwayomiModels.kt +++ /dev/null @@ -1,88 +0,0 @@ -package eu.kanade.tachiyomi.data.track.suwayomi - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -enum class MangaStatus( - val rawValue: String, -) { - UNKNOWN("UNKNOWN"), - ONGOING("ONGOING"), - COMPLETED("COMPLETED"), - LICENSED("LICENSED"), - PUBLISHING_FINISHED("PUBLISHING_FINISHED"), - CANCELLED("CANCELLED"), - ON_HIATUS("ON_HIATUS"), -} - -@Serializable -data class MangaFragment( - val artist: String?, - val author: String?, - val description: String?, - val id: Int, - val status: MangaStatus, - val thumbnailUrl: String?, - val title: String, - val url: String, - val genre: List, - val inLibraryAt: Long, - val chapters: Chapters, - val latestUploadedChapter: LatestUploadedChapter?, - val latestFetchedChapter: LatestFetchedChapter?, - val latestReadChapter: LatestReadChapter?, - val unreadCount: Int, - val downloadCount: Int, -) { - @Serializable - data class Chapters( - val totalCount: Int, - ) - - @Serializable - data class LatestUploadedChapter( - val uploadDate: Long, - ) - - @Serializable - data class LatestFetchedChapter( - val fetchedAt: Long, - ) - - @Serializable - data class LatestReadChapter( - val lastReadAt: Long, - val chapterNumber: Double, - ) -} - -@Serializable -data class GetMangaResult( - val data: GetMangaData, -) - -@Serializable -data class GetMangaData( - @SerialName("manga") val entry: MangaFragment, -) - -@Serializable -data class GetMangaUnreadChaptersEntry( - val nodes: List, -) - -@Serializable -data class GetMangaUnreadChaptersNode( - val id: Int, - val chapterNumber: Double, -) - -@Serializable -data class GetMangaUnreadChaptersResult( - val data: GetMangaUnreadChaptersData, -) - -@Serializable -data class GetMangaUnreadChaptersData( - @SerialName("chapters") val entry: GetMangaUnreadChaptersEntry, -) diff --git a/app/src/main/java/mihon/graphql/kitsu/adapter/KitsuMapAdapter.kt b/app/src/main/java/mihon/graphql/kitsu/adapter/KitsuMapAdapter.kt new file mode 100644 index 000000000..7da938cfd --- /dev/null +++ b/app/src/main/java/mihon/graphql/kitsu/adapter/KitsuMapAdapter.kt @@ -0,0 +1,26 @@ +package mihon.graphql.kitsu.adapter + +import com.apollographql.apollo.api.Adapter +import com.apollographql.apollo.api.AnyAdapter +import com.apollographql.apollo.api.CustomScalarAdapters +import com.apollographql.apollo.api.json.JsonReader +import com.apollographql.apollo.api.json.JsonWriter + +object KitsuMapAdapter : Adapter> { + + override fun fromJson( + reader: JsonReader, + customScalarAdapters: CustomScalarAdapters, + ): Map { + @Suppress("UNCHECKED_CAST") + return AnyAdapter.fromJson(reader, customScalarAdapters) as Map + } + + override fun toJson( + writer: JsonWriter, + customScalarAdapters: CustomScalarAdapters, + value: Map, + ) { + AnyAdapter.toJson(writer, customScalarAdapters, value) + } +} diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index b695b88e2..23beaaae7 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -33,6 +33,8 @@ dependencies { api(libs.okhttp.dnsOverHttps) api(libs.okio) + api(libs.apollo) + implementation(libs.image.decoder) implementation(libs.unifile) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ApolloExtensions.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ApolloExtensions.kt new file mode 100644 index 000000000..0d6fc9c5e --- /dev/null +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/ApolloExtensions.kt @@ -0,0 +1,56 @@ +package eu.kanade.tachiyomi.network + +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.exception.ApolloHttpException +import com.apollographql.apollo.exception.ApolloNetworkException +import logcat.LogPriority +import tachiyomi.core.common.util.system.logcat + +/** + * Returns the result of calling [transform] with [data] if the GraphQL response had data present without errors, + * otherwise logs the failure under [errorLog] and falls back to [default]. + * + * Any [ApolloHttpException] is re-thrown as [HttpException] after logging the response body. + * This behavior can be replaced with a custom handler by providing a non-null value to [onException]. Such a lambda + * MUST close the [ApolloHttpException.body] properly to avoid sockets and other resources leaking. + * + * Other failures are logged and the result of calling [default] is returned. + * + * @param errorLog A log message which will be included with any exceptions or errors being logged. + * @param default A lambda which provides a default value to be returned in place of the expected value when an + * exception or error is found. + * @param onException A handler for [ApolloHttpException]s. It MUST handle closing the response body to avoid resource + * leaks. Defaults to throwing a [HttpException] with the same status code. + * @param transform A transforming lambda for the GraphQL data received. + */ +fun ApolloResponse.dataOrElse( + errorLog: String, + default: () -> R, + onException: ((ApolloHttpException) -> Unit)? = null, + transform: (D) -> R, +): R { + if (exception != null) { + val e = exception!! + if (e is ApolloHttpException) { + val body = e.body?.use { it.readUtf8() } + logcat(LogPriority.ERROR, throwable = e) { "$errorLog: $body" } + if (onException != null) { + onException(e) + } else { + throw HttpException(e.statusCode).apply { stackTrace = e.stackTrace } + } + } + logcat(LogPriority.ERROR, throwable = e) { errorLog } + // Don't mask connection/IO-related issues + if (e is ApolloNetworkException && e.cause != null) { + throw e.cause!! + } + } else if (!errors.isNullOrEmpty()) { + val errorMessages = errors!!.joinToString(separator = "\n ", prefix = "\n ") { it.message } + logcat(LogPriority.ERROR) { "$errorLog: $errorMessages" } + } else if (data != null) { + return transform(data!!) + } + return default() +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6f4da98d..cfd279ce3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,8 @@ androidx-test-uiautomator = "2.4.0" androidx-viewPager = "1.1.0" androidx-webkit = "1.17.1" androidx-work = "2.12.0" +apollo = "5.2.0" +apollo-adapters = "0.8.0" archive = "1.1.7" coil = "3.6.3" composeGrid = "2.8.0" @@ -120,6 +122,8 @@ androidx-test-uiautomator = { module = "androidx.test.uiautomator:uiautomator", androidx-viewPager = { module = "androidx.viewpager:viewpager", version.ref = "androidx-viewPager" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "androidx-webkit" } androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } +apollo = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" } +apollo-adapters = { module = "com.apollographql.adapters:apollo-adapters-core", version.ref = "apollo-adapters" } archive = { module = "me.zhanghai.android.libarchive:library", version.ref = "archive" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" } coil-core = { module = "io.coil-kt.coil3:coil", version.ref = "coil" } @@ -204,6 +208,7 @@ android-kmp-library = { id = "com.android.kotlin.multiplatform.library", version android-library = { id = "com.android.library", version.ref = "android-gradle" } android-test = { id = "com.android.test", version.ref = "android-gradle" } androidx-baselineProfile = { id = "androidx.baselineprofile", version.ref = "androidx-baselineProfile" } +apollo = { id = "com.apollographql.apollo", version.ref = "apollo" } firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics" } google-services = { id = "com.google.gms.google-services", version.ref = "google-services" } kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin-gradle" }