ani-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/api/client.d.ts +45 -0
  4. package/dist/api/client.js +192 -0
  5. package/dist/api/queries.d.ts +30 -0
  6. package/dist/api/queries.js +401 -0
  7. package/dist/engine/compare.d.ts +18 -0
  8. package/dist/engine/compare.js +72 -0
  9. package/dist/engine/matcher.d.ts +12 -0
  10. package/dist/engine/matcher.js +146 -0
  11. package/dist/engine/mood.d.ts +17 -0
  12. package/dist/engine/mood.js +165 -0
  13. package/dist/engine/taste.d.ts +30 -0
  14. package/dist/engine/taste.js +202 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +26 -0
  17. package/dist/intelligence/matcher.d.ts +12 -0
  18. package/dist/intelligence/matcher.js +134 -0
  19. package/dist/intelligence/mood.d.ts +17 -0
  20. package/dist/intelligence/mood.js +125 -0
  21. package/dist/intelligence/taste.d.ts +30 -0
  22. package/dist/intelligence/taste.js +172 -0
  23. package/dist/schemas.d.ts +190 -0
  24. package/dist/schemas.js +316 -0
  25. package/dist/tools/discover.d.ts +4 -0
  26. package/dist/tools/discover.js +94 -0
  27. package/dist/tools/info.d.ts +4 -0
  28. package/dist/tools/info.js +172 -0
  29. package/dist/tools/lists.d.ts +4 -0
  30. package/dist/tools/lists.js +178 -0
  31. package/dist/tools/recommend.d.ts +4 -0
  32. package/dist/tools/recommend.js +405 -0
  33. package/dist/tools/search.d.ts +4 -0
  34. package/dist/tools/search.js +243 -0
  35. package/dist/tools/smart.d.ts +4 -0
  36. package/dist/tools/smart.js +311 -0
  37. package/dist/types.d.ts +303 -0
  38. package/dist/types.js +2 -0
  39. package/dist/utils.d.ts +12 -0
  40. package/dist/utils.js +69 -0
  41. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License Copyright (c) 2026 GavMason
2
+
3
+ Permission is hereby granted, free of
4
+ charge, to any person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use, copy, modify, merge,
7
+ publish, distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice
12
+ (including the next paragraph) shall be included in all copies or substantial
13
+ portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
18
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
19
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # ani-mcp
2
+
3
+ A smart [MCP](https://modelcontextprotocol.io) server for [AniList](https://anilist.co) that gets your anime/manga taste - not just API calls.
4
+
5
+ ## What makes this different
6
+
7
+ Most AniList integrations mirror the API 1:1. ani-mcp gives your AI assistant actual understanding of your watching habits:
8
+
9
+ - **anilist_pick** - "What should I watch next?" based on your taste profile and mood
10
+ - **anilist_taste** - Natural language summary of your anime/manga preferences
11
+ - **anilist_compare** - Compare taste between two users
12
+ - **anilist_wrapped** - Your year-in-review stats
13
+
14
+ Plus the essentials: search, details, trending, seasonal browsing, list management, and community recommendations.
15
+
16
+ ## Install
17
+
18
+ Add to your Claude Desktop config (`claude_desktop_config.json`) or `mcp.json`:
19
+
20
+ ```json
21
+ {
22
+ "mcpServers": {
23
+ "anilist": {
24
+ "command": "npx",
25
+ "args": ["-y", "ani-mcp"],
26
+ "env": {
27
+ "ANILIST_USERNAME": "your_username"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Environment Variables
35
+
36
+ | Variable | Required | Description |
37
+ | --- | --- | --- |
38
+ | `ANILIST_USERNAME` | No | Default username for list/stats tools. Can also pass per-call. |
39
+ | `ANILIST_TOKEN` | No | AniList OAuth token. Enables authenticated queries. |
40
+ | `DEBUG` | No | Set to `true` for debug logging to stderr. |
41
+
42
+ ## Tools
43
+
44
+ ### Search & Discovery
45
+
46
+ | Tool | Description |
47
+ | --- | --- |
48
+ | `anilist_search` | Search anime/manga by title with genre, year, and format filters |
49
+ | `anilist_details` | Full details, relations, and recommendations for a title |
50
+ | `anilist_seasonal` | Browse a season's anime lineup |
51
+ | `anilist_trending` | What's trending on AniList right now |
52
+ | `anilist_genres` | Browse top titles in a genre with optional filters |
53
+ | `anilist_recommendations` | Community recommendations for a specific title |
54
+
55
+ ### Lists & Stats
56
+
57
+ | Tool | Description |
58
+ | --- | --- |
59
+ | `anilist_list` | A user's anime/manga list, filtered by status |
60
+ | `anilist_stats` | Watching/reading statistics, top genres, score distribution |
61
+
62
+ ### Intelligence
63
+
64
+ | Tool | Description |
65
+ | --- | --- |
66
+ | `anilist_taste` | Generate a taste profile from your completed list |
67
+ | `anilist_pick` | Personalized "what to watch next" based on taste and mood |
68
+ | `anilist_compare` | Compare taste compatibility between two users |
69
+ | `anilist_wrapped` | Year-in-review summary |
70
+
71
+ ### Info
72
+
73
+ | Tool | Description |
74
+ | --- | --- |
75
+ | `anilist_staff` | Staff credits and voice actors for a title |
76
+ | `anilist_schedule` | Airing schedule and next episode countdown |
77
+ | `anilist_characters` | Search characters by name with appearances and VAs |
78
+
79
+ ## Build from Source
80
+
81
+ ```sh
82
+ git clone https://github.com/GavMason/ani-mcp.git
83
+ cd ani-mcp
84
+ npm install
85
+ npm run build
86
+ npm test
87
+ ```
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,45 @@
1
+ /**
2
+ * AniList GraphQL API Client
3
+ *
4
+ * Handles rate limiting (token bucket), retry with exponential backoff,
5
+ * and in-memory caching.
6
+ */
7
+ import type { AniListMediaListEntry } from "../types.js";
8
+ /** Per-category TTLs for the query cache */
9
+ export declare const CACHE_TTLS: {
10
+ readonly media: number;
11
+ readonly search: number;
12
+ readonly list: number;
13
+ readonly seasonal: number;
14
+ readonly stats: number;
15
+ };
16
+ export type CacheCategory = keyof typeof CACHE_TTLS;
17
+ /** API error with HTTP status and retry eligibility */
18
+ export declare class AniListApiError extends Error {
19
+ readonly status?: number | undefined;
20
+ readonly retryable: boolean;
21
+ constructor(message: string, status?: number | undefined, retryable?: boolean);
22
+ }
23
+ /** Options for a single query call */
24
+ export interface QueryOptions {
25
+ /** Cache category to use. Pass null to skip caching. */
26
+ cache?: CacheCategory | null;
27
+ }
28
+ /** Manages authenticated requests to the AniList GraphQL API */
29
+ declare class AniListClient {
30
+ private token;
31
+ constructor();
32
+ /** Execute a GraphQL query with caching and automatic retry */
33
+ query<T = unknown>(query: string, variables?: Record<string, unknown>, options?: QueryOptions): Promise<T>;
34
+ /** Fetch a user's media list, flattened into a single array */
35
+ fetchList(username: string, type: string, status?: string, sort?: string[]): Promise<AniListMediaListEntry[]>;
36
+ /** Invalidate the entire query cache */
37
+ clearCache(): void;
38
+ /** Retries with exponential backoff via p-retry */
39
+ private executeWithRetry;
40
+ /** Send a single GraphQL POST request and parse the response */
41
+ private makeRequest;
42
+ }
43
+ /** Singleton. Rate limiter and cache must be shared across all tools. */
44
+ export declare const anilistClient: AniListClient;
45
+ export {};
@@ -0,0 +1,192 @@
1
+ /**
2
+ * AniList GraphQL API Client
3
+ *
4
+ * Handles rate limiting (token bucket), retry with exponential backoff,
5
+ * and in-memory caching.
6
+ */
7
+ import { LRUCache } from "lru-cache";
8
+ import pRetry, { AbortError } from "p-retry";
9
+ import pThrottle from "p-throttle";
10
+ import { USER_LIST_QUERY } from "./queries.js";
11
+ const ANILIST_API_URL = process.env.ANILIST_API_URL || "https://graphql.anilist.co";
12
+ // Budget under the 90 req/min limit to leave headroom
13
+ const RATE_LIMIT_PER_MINUTE = 85;
14
+ const MAX_RETRIES = 3;
15
+ // Hard timeout per fetch attempt (retries get their own timeout)
16
+ const FETCH_TIMEOUT_MS = 15_000;
17
+ // === Logging ===
18
+ const DEBUG = process.env.DEBUG === "true" || process.env.DEBUG === "1";
19
+ // Extract query operation name (e.g. "SearchMedia" from "query SearchMedia(...)")
20
+ function queryName(query) {
21
+ const match = query.match(/(?:query|mutation)\s+(\w+)/);
22
+ return match ? match[1] : "unknown";
23
+ }
24
+ function log(event, detail) {
25
+ if (!DEBUG)
26
+ return;
27
+ const msg = detail ? `[ani-mcp] ${event}: ${detail}` : `[ani-mcp] ${event}`;
28
+ console.error(msg);
29
+ }
30
+ /** Per-category TTLs for the query cache */
31
+ export const CACHE_TTLS = {
32
+ media: 60 * 60 * 1000, // 1h
33
+ search: 2 * 60 * 1000, // 2m
34
+ list: 5 * 60 * 1000, // 5m
35
+ seasonal: 30 * 60 * 1000, // 30m
36
+ stats: 10 * 60 * 1000, // 10m
37
+ };
38
+ // 85 req/60s, excess calls queue automatically
39
+ const rateLimit = pThrottle({
40
+ limit: RATE_LIMIT_PER_MINUTE,
41
+ interval: 60_000,
42
+ })(() => { });
43
+ // === In-Memory Cache ===
44
+ /** LRU cache with per-entry TTL, keyed on query + variables */
45
+ const queryCache = new LRUCache({
46
+ max: 500,
47
+ allowStale: false,
48
+ });
49
+ // === Error Types ===
50
+ /** API error with HTTP status and retry eligibility */
51
+ export class AniListApiError extends Error {
52
+ status;
53
+ retryable;
54
+ constructor(message, status, retryable = false) {
55
+ super(message);
56
+ this.status = status;
57
+ this.retryable = retryable;
58
+ this.name = "AniListApiError";
59
+ }
60
+ }
61
+ /** Manages authenticated requests to the AniList GraphQL API */
62
+ class AniListClient {
63
+ token;
64
+ constructor() {
65
+ // Optional - unauthenticated requests still work for public data
66
+ this.token = process.env.ANILIST_TOKEN || undefined;
67
+ }
68
+ /** Execute a GraphQL query with caching and automatic retry */
69
+ async query(query, variables = {}, options = {}) {
70
+ const cacheCategory = options.cache ?? null;
71
+ const name = queryName(query);
72
+ // Cache-through: return cached result or fetch, store, and return
73
+ if (cacheCategory) {
74
+ const cacheKey = `${query}::${JSON.stringify(variables)}`;
75
+ const cached = queryCache.get(cacheKey);
76
+ if (cached !== undefined) {
77
+ log("cache-hit", name);
78
+ return cached;
79
+ }
80
+ log("cache-miss", name);
81
+ const data = await this.executeWithRetry(query, variables);
82
+ queryCache.set(cacheKey, data, {
83
+ ttl: CACHE_TTLS[cacheCategory],
84
+ });
85
+ return data;
86
+ }
87
+ // No cache category - skip caching entirely
88
+ return this.executeWithRetry(query, variables);
89
+ }
90
+ /** Fetch a user's media list, flattened into a single array */
91
+ async fetchList(username, type, status, sort) {
92
+ const variables = { userName: username, type };
93
+ if (status)
94
+ variables.status = status;
95
+ if (sort)
96
+ variables.sort = sort;
97
+ const data = await this.query(USER_LIST_QUERY, variables, { cache: "list" });
98
+ // Flatten across status groups
99
+ const entries = [];
100
+ for (const list of data.MediaListCollection.lists) {
101
+ entries.push(...list.entries);
102
+ }
103
+ return entries;
104
+ }
105
+ /** Invalidate the entire query cache */
106
+ clearCache() {
107
+ queryCache.clear();
108
+ }
109
+ /** Retries with exponential backoff via p-retry */
110
+ async executeWithRetry(query, variables) {
111
+ const name = queryName(query);
112
+ log("fetch", name);
113
+ return pRetry(async () => {
114
+ await rateLimit();
115
+ return this.makeRequest(query, variables);
116
+ }, {
117
+ retries: MAX_RETRIES,
118
+ onFailedAttempt: (err) => {
119
+ log("retry", `${name} attempt ${err.attemptNumber}/${MAX_RETRIES + 1}`);
120
+ },
121
+ });
122
+ }
123
+ /** Send a single GraphQL POST request and parse the response */
124
+ async makeRequest(query, variables) {
125
+ const headers = {
126
+ "Content-Type": "application/json",
127
+ Accept: "application/json",
128
+ };
129
+ // Attach auth header if an OAuth token is configured
130
+ if (this.token) {
131
+ headers["Authorization"] = `Bearer ${this.token}`;
132
+ }
133
+ // Network errors (DNS, timeout, etc.) are retryable
134
+ let response;
135
+ try {
136
+ response = await fetch(ANILIST_API_URL, {
137
+ method: "POST",
138
+ headers,
139
+ body: JSON.stringify({ query, variables }),
140
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
141
+ });
142
+ }
143
+ catch (error) {
144
+ const msg = error instanceof Error ? error.message : String(error);
145
+ log("network-error", msg);
146
+ throw new AniListApiError(`Network error connecting to AniList: ${msg}`, undefined, true);
147
+ }
148
+ // Map HTTP errors to retryable/non-retryable
149
+ if (!response.ok) {
150
+ // Read error body for context
151
+ const body = await response.text().catch(() => "");
152
+ if (response.status === 429) {
153
+ log("rate-limit", `429 from AniList`);
154
+ const retryAfter = response.headers.get("Retry-After");
155
+ if (retryAfter) {
156
+ const delaySec = parseInt(retryAfter, 10);
157
+ if (delaySec > 0) {
158
+ await new Promise((r) => setTimeout(r, delaySec * 1000));
159
+ }
160
+ }
161
+ throw new AniListApiError("AniList rate limit hit. The server will retry automatically.", 429, true);
162
+ }
163
+ if (response.status === 404) {
164
+ throw new AbortError(new AniListApiError("Resource not found on AniList. Check that the ID or username is correct.", 404, false));
165
+ }
166
+ // Only server errors (5xx) are worth retrying
167
+ if (response.status >= 500) {
168
+ throw new AniListApiError(`AniList API error (HTTP ${response.status}): ${body.slice(0, 200)}`, response.status, true);
169
+ }
170
+ // Client errors (4xx except 429) are not worth retrying
171
+ throw new AbortError(new AniListApiError(`AniList API error (HTTP ${response.status}): ${body.slice(0, 200)}`, response.status, false));
172
+ }
173
+ // AniList can return both data and errors
174
+ const json = (await response.json());
175
+ // GraphQL can return 200 OK with errors in the body
176
+ if (json.errors?.length) {
177
+ // Prefer GraphQL error status over HTTP status
178
+ const firstError = json.errors[0];
179
+ const status = firstError.status ?? response.status;
180
+ const retryable = status === 429 || (status !== undefined && status >= 500);
181
+ const err = new AniListApiError(`AniList GraphQL error: ${firstError.message}`, status, retryable);
182
+ throw retryable ? err : new AbortError(err);
183
+ }
184
+ // Guard against empty response
185
+ if (!json.data) {
186
+ throw new AniListApiError("AniList returned an empty response. Try again.");
187
+ }
188
+ return json.data;
189
+ }
190
+ }
191
+ /** Singleton. Rate limiter and cache must be shared across all tools. */
192
+ export const anilistClient = new AniListClient();
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AniList GraphQL Query Strings
3
+ *
4
+ * Separated from tool logic so queries are easy to find and update
5
+ * if the AniList schema changes.
6
+ */
7
+ /** Paginated search with optional genre, year, and format filters */
8
+ export declare const SEARCH_MEDIA_QUERY = "\n query SearchMedia(\n $search: String!\n $type: MediaType\n $genre: [String]\n $year: Int\n $format: MediaFormat\n $isAdult: Boolean\n $page: Int\n $perPage: Int\n $sort: [MediaSort]\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo {\n total\n currentPage\n lastPage\n hasNextPage\n }\n media(\n search: $search\n type: $type\n genre_in: $genre\n startDate_year: $year\n format: $format\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
9
+ /** Full media lookup with relations and recommendations */
10
+ export declare const MEDIA_DETAILS_QUERY = "\n query MediaDetails($id: Int, $search: String) {\n Media(id: $id, search: $search) {\n ...MediaFields\n relations {\n edges {\n relationType\n node {\n id\n title { romaji english }\n format\n status\n type\n }\n }\n }\n recommendations(sort: RATING_DESC, perPage: 5) {\n nodes {\n rating\n mediaRecommendation {\n id\n title { romaji english }\n format\n meanScore\n genres\n siteUrl\n }\n }\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
11
+ /** Discover top-rated titles by genre without a search term */
12
+ export declare const DISCOVER_MEDIA_QUERY = "\n query DiscoverMedia(\n $type: MediaType\n $genre_in: [String]\n $page: Int\n $perPage: Int\n $sort: [MediaSort]\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(type: $type, genre_in: $genre_in, sort: $sort) {\n ...MediaFields\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
13
+ /** Browse anime by season and year */
14
+ export declare const SEASONAL_MEDIA_QUERY = "\n query SeasonalMedia(\n $season: MediaSeason\n $seasonYear: Int\n $type: MediaType\n $isAdult: Boolean\n $sort: [MediaSort]\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total currentPage lastPage hasNextPage }\n media(\n season: $season\n seasonYear: $seasonYear\n type: $type\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
15
+ /** User profile statistics - watching/reading stats, genre/tag/score breakdowns */
16
+ export declare const USER_STATS_QUERY = "\n query UserStats($name: String!) {\n User(name: $name) {\n id\n name\n statistics {\n anime {\n count\n meanScore\n minutesWatched\n episodesWatched\n genres(sort: COUNT_DESC, limit: 10) {\n genre\n count\n meanScore\n minutesWatched\n }\n scores(sort: MEAN_SCORE_DESC) {\n score\n count\n }\n formats(sort: COUNT_DESC) {\n format\n count\n }\n }\n manga {\n count\n meanScore\n chaptersRead\n volumesRead\n genres(sort: COUNT_DESC, limit: 10) {\n genre\n count\n meanScore\n chaptersRead\n }\n scores(sort: MEAN_SCORE_DESC) {\n score\n count\n }\n formats(sort: COUNT_DESC) {\n format\n count\n }\n }\n }\n }\n }\n";
17
+ /** Media recommendations for a given title */
18
+ export declare const RECOMMENDATIONS_QUERY = "\n query MediaRecommendations($id: Int, $search: String, $perPage: Int) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n recommendations(sort: RATING_DESC, perPage: $perPage) {\n nodes {\n rating\n mediaRecommendation {\n ...MediaFields\n }\n }\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
19
+ /** Trending anime or manga right now */
20
+ export declare const TRENDING_MEDIA_QUERY = "\n query TrendingMedia(\n $type: MediaType\n $isAdult: Boolean\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(type: $type, isAdult: $isAdult, sort: TRENDING_DESC) {\n ...MediaFields\n trending\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
21
+ /** Browse by genre without a search term, with optional filters */
22
+ export declare const GENRE_BROWSE_QUERY = "\n query GenreBrowse(\n $type: MediaType\n $genre_in: [String]\n $year: Int\n $status: MediaStatus\n $format: MediaFormat\n $isAdult: Boolean\n $sort: [MediaSort]\n $page: Int\n $perPage: Int\n ) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n media(\n type: $type\n genre_in: $genre_in\n startDate_year: $year\n status: $status\n format: $format\n isAdult: $isAdult\n sort: $sort\n ) {\n ...MediaFields\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";
23
+ /** Staff and voice actors for a media title */
24
+ export declare const STAFF_QUERY = "\n query MediaStaff($id: Int, $search: String) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n format\n siteUrl\n staff(sort: RELEVANCE, perPage: 15) {\n edges {\n role\n node {\n id\n name { full native }\n siteUrl\n }\n }\n }\n characters(sort: ROLE, perPage: 10) {\n edges {\n role\n node {\n id\n name { full native }\n siteUrl\n }\n voiceActors(language: JAPANESE) {\n id\n name { full native }\n siteUrl\n }\n }\n }\n }\n }\n";
25
+ /** Airing schedule for currently airing anime */
26
+ export declare const AIRING_SCHEDULE_QUERY = "\n query AiringSchedule($id: Int, $search: String, $notYetAired: Boolean) {\n Media(id: $id, search: $search) {\n id\n title { romaji english native }\n status\n episodes\n nextAiringEpisode {\n episode\n airingAt\n timeUntilAiring\n }\n airingSchedule(notYetAired: $notYetAired, perPage: 10) {\n nodes {\n episode\n airingAt\n timeUntilAiring\n }\n }\n siteUrl\n }\n }\n";
27
+ /** Search for characters by name */
28
+ export declare const CHARACTER_SEARCH_QUERY = "\n query CharacterSearch($search: String!, $page: Int, $perPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n characters(search: $search, sort: FAVOURITES_DESC) {\n id\n name { full native alternative }\n image { medium }\n favourites\n siteUrl\n media(sort: POPULARITY_DESC, perPage: 5) {\n edges {\n characterRole\n node {\n id\n title { romaji english }\n format\n type\n siteUrl\n }\n voiceActors(language: JAPANESE) {\n id\n name { full }\n siteUrl\n }\n }\n }\n }\n }\n }\n";
29
+ /** User's anime/manga list, grouped by status. Omit $status to get all lists. */
30
+ export declare const USER_LIST_QUERY = "\n query UserMediaList(\n $userName: String!\n $type: MediaType\n $status: MediaListStatus\n $sort: [MediaListSort]\n ) {\n MediaListCollection(\n userName: $userName\n type: $type\n status: $status\n sort: $sort\n ) {\n lists {\n name\n status\n entries {\n id\n score(format: POINT_10) # normalize to 1-10 scale regardless of user's profile setting\n progress\n status\n updatedAt\n startedAt { year month day }\n completedAt { year month day }\n notes\n media {\n ...MediaFields\n }\n }\n }\n }\n }\n \n fragment MediaFields on Media {\n id\n type\n title {\n romaji\n english\n native\n }\n format\n status\n episodes\n chapters\n volumes\n meanScore\n averageScore\n popularity\n genres\n tags {\n name\n rank\n isMediaSpoiler\n }\n season\n seasonYear\n startDate { year month day }\n endDate { year month day }\n studios(isMain: true) {\n nodes { name }\n }\n source\n isAdult\n coverImage { large }\n siteUrl\n description(asHtml: false)\n }\n\n";