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.
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/api/client.d.ts +45 -0
- package/dist/api/client.js +192 -0
- package/dist/api/queries.d.ts +30 -0
- package/dist/api/queries.js +401 -0
- package/dist/engine/compare.d.ts +18 -0
- package/dist/engine/compare.js +72 -0
- package/dist/engine/matcher.d.ts +12 -0
- package/dist/engine/matcher.js +146 -0
- package/dist/engine/mood.d.ts +17 -0
- package/dist/engine/mood.js +165 -0
- package/dist/engine/taste.d.ts +30 -0
- package/dist/engine/taste.js +202 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +26 -0
- package/dist/intelligence/matcher.d.ts +12 -0
- package/dist/intelligence/matcher.js +134 -0
- package/dist/intelligence/mood.d.ts +17 -0
- package/dist/intelligence/mood.js +125 -0
- package/dist/intelligence/taste.d.ts +30 -0
- package/dist/intelligence/taste.js +172 -0
- package/dist/schemas.d.ts +190 -0
- package/dist/schemas.js +316 -0
- package/dist/tools/discover.d.ts +4 -0
- package/dist/tools/discover.js +94 -0
- package/dist/tools/info.d.ts +4 -0
- package/dist/tools/info.js +172 -0
- package/dist/tools/lists.d.ts +4 -0
- package/dist/tools/lists.js +178 -0
- package/dist/tools/recommend.d.ts +4 -0
- package/dist/tools/recommend.js +405 -0
- package/dist/tools/search.d.ts +4 -0
- package/dist/tools/search.js +243 -0
- package/dist/tools/smart.d.ts +4 -0
- package/dist/tools/smart.js +311 -0
- package/dist/types.d.ts +303 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +69 -0
- 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";
|