ani-mcp 0.8.4 → 0.10.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/README.md +14 -0
- package/dist/api/client.js +1 -1
- package/dist/api/mal-client.d.ts +23 -0
- package/dist/api/mal-client.js +61 -0
- package/dist/api/queries.d.ts +12 -10
- package/dist/api/queries.js +26 -1
- package/dist/engine/card.d.ts +26 -0
- package/dist/engine/card.js +407 -0
- package/dist/engine/franchise.d.ts +6 -1
- package/dist/engine/franchise.js +2 -1
- package/dist/engine/undo.d.ts +1 -0
- package/dist/index.js +5 -1
- package/dist/schemas.d.ts +38 -0
- package/dist/schemas.js +62 -0
- package/dist/tools/cards.d.ts +4 -0
- package/dist/tools/cards.js +137 -0
- package/dist/tools/discover.js +31 -23
- package/dist/tools/import.d.ts +4 -0
- package/dist/tools/import.js +134 -0
- package/dist/tools/info.js +73 -3
- package/dist/tools/lists.js +6 -2
- package/dist/tools/recommend.js +4 -1
- package/dist/tools/search.js +9 -1
- package/dist/tools/write.js +11 -1
- package/dist/types.d.ts +30 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +18 -0
- package/manifest.json +7 -1
- package/package.json +4 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -95,6 +95,19 @@ Works with any MCP-compatible client.
|
|
|
95
95
|
| `anilist_sequels` | Sequels airing this season for titles you've completed |
|
|
96
96
|
| `anilist_watch_order` | Viewing order for a franchise |
|
|
97
97
|
| `anilist_session` | Plan a viewing session within a time budget |
|
|
98
|
+
| `anilist_mal_import` | Import a MyAnimeList user's list and generate recommendations |
|
|
99
|
+
|
|
100
|
+
### Cards
|
|
101
|
+
|
|
102
|
+
| Tool | Description |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `anilist_taste_card` | Generate a shareable taste profile card as a PNG image |
|
|
105
|
+
| `anilist_compat_card` | Generate a compatibility card comparing two users as a PNG image |
|
|
106
|
+
|
|
107
|
+
<p align="center">
|
|
108
|
+
<img src="assets/taste-card.png" width="400" alt="Taste Profile Card">
|
|
109
|
+
<img src="assets/compat-card.png" width="400" alt="Compatibility Card">
|
|
110
|
+
</p>
|
|
98
111
|
|
|
99
112
|
### Info
|
|
100
113
|
|
|
@@ -104,6 +117,7 @@ Works with any MCP-compatible client.
|
|
|
104
117
|
| `anilist_staff_search` | Search for a person by name and see all their works |
|
|
105
118
|
| `anilist_studio_search` | Search for a studio and see their productions |
|
|
106
119
|
| `anilist_schedule` | Airing schedule and next episode countdown |
|
|
120
|
+
| `anilist_airing` | Upcoming episodes for titles you're currently watching |
|
|
107
121
|
| `anilist_characters` | Search characters by name with appearances and VAs |
|
|
108
122
|
| `anilist_whoami` | Check authentication status and score format |
|
|
109
123
|
|
package/dist/api/client.js
CHANGED
|
@@ -13,7 +13,7 @@ const ANILIST_API_URL = process.env.ANILIST_API_URL || "https://graphql.anilist.
|
|
|
13
13
|
const RATE_LIMIT_PER_MINUTE = process.env.VITEST ? 10_000 : 85;
|
|
14
14
|
const MAX_RETRIES = 3;
|
|
15
15
|
// Hard timeout per fetch attempt (retries get their own timeout)
|
|
16
|
-
const FETCH_TIMEOUT_MS = 15_000;
|
|
16
|
+
const FETCH_TIMEOUT_MS = process.env.VITEST ? 500 : 15_000;
|
|
17
17
|
// === Logging ===
|
|
18
18
|
const DEBUG = process.env.DEBUG === "true" || process.env.DEBUG === "1";
|
|
19
19
|
// Extract query operation name (e.g. "SearchMedia" from "query SearchMedia(...)")
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Jikan (MyAnimeList) API client for read-only list import */
|
|
2
|
+
export interface JikanAnimeEntry {
|
|
3
|
+
score: number;
|
|
4
|
+
episodes_watched: number;
|
|
5
|
+
anime: {
|
|
6
|
+
mal_id: number;
|
|
7
|
+
title: string;
|
|
8
|
+
type: string;
|
|
9
|
+
episodes: number | null;
|
|
10
|
+
score: number | null;
|
|
11
|
+
genres: Array<{
|
|
12
|
+
mal_id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
}>;
|
|
15
|
+
year: number | null;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Map a MAL genre name to its AniList equivalent */
|
|
19
|
+
export declare function mapMalGenre(name: string): string;
|
|
20
|
+
/** Map a MAL format string to AniList format */
|
|
21
|
+
export declare function mapMalFormat(malType: string): string;
|
|
22
|
+
/** Fetch a MAL user's completed anime list via Jikan */
|
|
23
|
+
export declare function fetchMalList(username: string, maxPages?: number): Promise<JikanAnimeEntry[]>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Jikan (MyAnimeList) API client for read-only list import */
|
|
2
|
+
import pThrottle from "p-throttle";
|
|
3
|
+
import pRetry, { AbortError } from "p-retry";
|
|
4
|
+
const JIKAN_BASE = "https://api.jikan.moe/v4";
|
|
5
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
6
|
+
// Jikan rate limit: 3 req/sec (unauthenticated)
|
|
7
|
+
const throttle = pThrottle({
|
|
8
|
+
limit: process.env.VITEST ? 10_000 : 3,
|
|
9
|
+
interval: 1_000,
|
|
10
|
+
});
|
|
11
|
+
const throttled = throttle(() => { });
|
|
12
|
+
// === Genre Mapping ===
|
|
13
|
+
// MAL and AniList share most genre names; only map exceptions
|
|
14
|
+
const MAL_TO_ANILIST_GENRE = {
|
|
15
|
+
"Sci-Fi": "Sci-Fi",
|
|
16
|
+
"Slice of Life": "Slice of Life",
|
|
17
|
+
};
|
|
18
|
+
/** Map a MAL genre name to its AniList equivalent */
|
|
19
|
+
export function mapMalGenre(name) {
|
|
20
|
+
return MAL_TO_ANILIST_GENRE[name] ?? name;
|
|
21
|
+
}
|
|
22
|
+
/** Map a MAL format string to AniList format */
|
|
23
|
+
export function mapMalFormat(malType) {
|
|
24
|
+
const map = {
|
|
25
|
+
TV: "TV",
|
|
26
|
+
Movie: "MOVIE",
|
|
27
|
+
OVA: "OVA",
|
|
28
|
+
ONA: "ONA",
|
|
29
|
+
Special: "SPECIAL",
|
|
30
|
+
Music: "MUSIC",
|
|
31
|
+
};
|
|
32
|
+
return map[malType] ?? "TV";
|
|
33
|
+
}
|
|
34
|
+
// === Client ===
|
|
35
|
+
/** Fetch a MAL user's completed anime list via Jikan */
|
|
36
|
+
export async function fetchMalList(username, maxPages = 5) {
|
|
37
|
+
const entries = [];
|
|
38
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
39
|
+
const data = await fetchPage(username, page);
|
|
40
|
+
entries.push(...data.data);
|
|
41
|
+
if (!data.pagination.has_next_page)
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
return entries;
|
|
45
|
+
}
|
|
46
|
+
async function fetchPage(username, page) {
|
|
47
|
+
return pRetry(async () => {
|
|
48
|
+
await throttled();
|
|
49
|
+
const url = `${JIKAN_BASE}/users/${encodeURIComponent(username)}/animelist?status=completed&page=${page}`;
|
|
50
|
+
const response = await fetch(url, {
|
|
51
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
52
|
+
});
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
if (response.status === 404) {
|
|
55
|
+
throw new AbortError(`MAL user "${username}" not found.`);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Jikan API error (HTTP ${response.status})`);
|
|
58
|
+
}
|
|
59
|
+
return (await response.json());
|
|
60
|
+
}, { retries: 3 });
|
|
61
|
+
}
|
package/dist/api/queries.d.ts
CHANGED
|
@@ -5,35 +5,37 @@
|
|
|
5
5
|
* if the AniList schema changes.
|
|
6
6
|
*/
|
|
7
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
9
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
11
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
13
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
15
15
|
/** User profile statistics - watching/reading stats, genre/tag/score breakdowns */
|
|
16
16
|
export declare const USER_STATS_QUERY = "\n query UserStats($name: String!) {\n User(name: $name) {\n id\n name\n mediaListOptions {\n scoreFormat\n }\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
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
19
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
21
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 duration\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";
|
|
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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
23
23
|
/** Staff and voice actors for a media title */
|
|
24
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
25
|
/** Airing schedule for currently airing anime */
|
|
26
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
|
+
/** Batch-fetch next airing episodes for multiple media IDs */
|
|
28
|
+
export declare const BATCH_AIRING_QUERY = "\n query BatchAiring($ids: [Int], $perPage: Int) {\n Page(perPage: $perPage) {\n media(id_in: $ids, status: RELEASING) {\n id\n title { romaji english native }\n format\n episodes\n nextAiringEpisode {\n episode\n airingAt\n timeUntilAiring\n }\n siteUrl\n }\n }\n }\n";
|
|
27
29
|
/** Search for characters by name */
|
|
28
30
|
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
31
|
/** Create or update a list entry */
|
|
30
|
-
export declare const SAVE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation SaveMediaListEntry(\n $mediaId: Int\n $status: MediaListStatus\n $scoreRaw: Int\n $progress: Int\n $notes: String\n $private: Boolean\n ) {\n SaveMediaListEntry(\n mediaId: $mediaId\n status: $status\n scoreRaw: $scoreRaw\n progress: $progress\n notes: $notes\n private: $private\n ) {\n id\n mediaId\n status\n score(format: POINT_10)\n progress\n }\n }\n";
|
|
32
|
+
export declare const SAVE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation SaveMediaListEntry(\n $mediaId: Int\n $status: MediaListStatus\n $scoreRaw: Int\n $progress: Int\n $progressVolumes: Int\n $notes: String\n $private: Boolean\n ) {\n SaveMediaListEntry(\n mediaId: $mediaId\n status: $status\n scoreRaw: $scoreRaw\n progress: $progress\n progressVolumes: $progressVolumes\n notes: $notes\n private: $private\n ) {\n id\n mediaId\n status\n score(format: POINT_10)\n progress\n progressVolumes\n }\n }\n";
|
|
31
33
|
/** Fetch a single list entry for snapshotting before mutations */
|
|
32
|
-
export declare const MEDIA_LIST_ENTRY_QUERY = "\n query MediaListEntry($id: Int, $mediaId: Int, $userName: String) {\n MediaList(id: $id, mediaId: $mediaId, userName: $userName) {\n id\n mediaId\n status\n score(format: POINT_10)\n progress\n notes\n private\n }\n }\n";
|
|
34
|
+
export declare const MEDIA_LIST_ENTRY_QUERY = "\n query MediaListEntry($id: Int, $mediaId: Int, $userName: String) {\n MediaList(id: $id, mediaId: $mediaId, userName: $userName) {\n id\n mediaId\n status\n score(format: POINT_10)\n progress\n progressVolumes\n notes\n private\n }\n }\n";
|
|
33
35
|
/** Remove a list entry */
|
|
34
36
|
export declare const DELETE_MEDIA_LIST_ENTRY_MUTATION = "\n mutation DeleteMediaListEntry($id: Int!) {\n DeleteMediaListEntry(id: $id) {\n deleted\n }\n }\n";
|
|
35
37
|
/** User's anime/manga list, grouped by status. Omit $status to get all lists. */
|
|
36
|
-
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 isCustomList\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 duration\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";
|
|
38
|
+
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 isCustomList\n entries {\n id\n score(format: POINT_10) # normalize to 1-10 scale regardless of user's profile setting\n progress\n progressVolumes\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 duration\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 extraLarge }\n trailer { id site thumbnail }\n siteUrl\n description(asHtml: false)\n }\n\n";
|
|
37
39
|
/** Search for staff by name with their top works */
|
|
38
40
|
export declare const STAFF_SEARCH_QUERY = "\n query StaffSearch($search: String!, $page: Int, $perPage: Int, $mediaPerPage: Int) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total hasNextPage }\n staff(search: $search, sort: SEARCH_MATCH) {\n id\n name { full native }\n primaryOccupations\n siteUrl\n staffMedia(sort: POPULARITY_DESC, perPage: $mediaPerPage) {\n edges {\n staffRole\n node {\n id\n title { romaji english }\n format\n type\n meanScore\n siteUrl\n }\n }\n }\n }\n }\n }\n";
|
|
39
41
|
/** Authenticated user info */
|
package/dist/api/queries.js
CHANGED
|
@@ -38,7 +38,8 @@ const MEDIA_FRAGMENT = `
|
|
|
38
38
|
}
|
|
39
39
|
source
|
|
40
40
|
isAdult
|
|
41
|
-
coverImage { large }
|
|
41
|
+
coverImage { large extraLarge }
|
|
42
|
+
trailer { id site thumbnail }
|
|
42
43
|
siteUrl
|
|
43
44
|
description(asHtml: false)
|
|
44
45
|
}
|
|
@@ -336,6 +337,25 @@ export const AIRING_SCHEDULE_QUERY = `
|
|
|
336
337
|
}
|
|
337
338
|
}
|
|
338
339
|
`;
|
|
340
|
+
/** Batch-fetch next airing episodes for multiple media IDs */
|
|
341
|
+
export const BATCH_AIRING_QUERY = `
|
|
342
|
+
query BatchAiring($ids: [Int], $perPage: Int) {
|
|
343
|
+
Page(perPage: $perPage) {
|
|
344
|
+
media(id_in: $ids, status: RELEASING) {
|
|
345
|
+
id
|
|
346
|
+
title { romaji english native }
|
|
347
|
+
format
|
|
348
|
+
episodes
|
|
349
|
+
nextAiringEpisode {
|
|
350
|
+
episode
|
|
351
|
+
airingAt
|
|
352
|
+
timeUntilAiring
|
|
353
|
+
}
|
|
354
|
+
siteUrl
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
339
359
|
/** Search for characters by name */
|
|
340
360
|
export const CHARACTER_SEARCH_QUERY = `
|
|
341
361
|
query CharacterSearch($search: String!, $page: Int, $perPage: Int) {
|
|
@@ -375,6 +395,7 @@ export const SAVE_MEDIA_LIST_ENTRY_MUTATION = `
|
|
|
375
395
|
$status: MediaListStatus
|
|
376
396
|
$scoreRaw: Int
|
|
377
397
|
$progress: Int
|
|
398
|
+
$progressVolumes: Int
|
|
378
399
|
$notes: String
|
|
379
400
|
$private: Boolean
|
|
380
401
|
) {
|
|
@@ -383,6 +404,7 @@ export const SAVE_MEDIA_LIST_ENTRY_MUTATION = `
|
|
|
383
404
|
status: $status
|
|
384
405
|
scoreRaw: $scoreRaw
|
|
385
406
|
progress: $progress
|
|
407
|
+
progressVolumes: $progressVolumes
|
|
386
408
|
notes: $notes
|
|
387
409
|
private: $private
|
|
388
410
|
) {
|
|
@@ -391,6 +413,7 @@ export const SAVE_MEDIA_LIST_ENTRY_MUTATION = `
|
|
|
391
413
|
status
|
|
392
414
|
score(format: POINT_10)
|
|
393
415
|
progress
|
|
416
|
+
progressVolumes
|
|
394
417
|
}
|
|
395
418
|
}
|
|
396
419
|
`;
|
|
@@ -403,6 +426,7 @@ export const MEDIA_LIST_ENTRY_QUERY = `
|
|
|
403
426
|
status
|
|
404
427
|
score(format: POINT_10)
|
|
405
428
|
progress
|
|
429
|
+
progressVolumes
|
|
406
430
|
notes
|
|
407
431
|
private
|
|
408
432
|
}
|
|
@@ -438,6 +462,7 @@ export const USER_LIST_QUERY = `
|
|
|
438
462
|
id
|
|
439
463
|
score(format: POINT_10) # normalize to 1-10 scale regardless of user's profile setting
|
|
440
464
|
progress
|
|
465
|
+
progressVolumes
|
|
441
466
|
status
|
|
442
467
|
updatedAt
|
|
443
468
|
startedAt { year month day }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Generates shareable SVG cards for taste profiles and compatibility comparisons */
|
|
2
|
+
import type { TasteProfile } from "./taste.js";
|
|
3
|
+
/** Fetch an image URL and return a base64 data URI, or null on failure */
|
|
4
|
+
export declare function fetchAvatarB64(url: string): Promise<string | null>;
|
|
5
|
+
/** Build an SVG taste profile card */
|
|
6
|
+
export declare function buildTasteCardSvg(username: string, profile: TasteProfile, avatarB64?: string | null): string;
|
|
7
|
+
export interface CompatCardData {
|
|
8
|
+
user1: string;
|
|
9
|
+
user2: string;
|
|
10
|
+
compatibility: number;
|
|
11
|
+
sharedCount: number;
|
|
12
|
+
sharedFavorites: Array<{
|
|
13
|
+
title: string;
|
|
14
|
+
score1: number;
|
|
15
|
+
score2: number;
|
|
16
|
+
}>;
|
|
17
|
+
divergences: string[];
|
|
18
|
+
profile1: TasteProfile;
|
|
19
|
+
profile2: TasteProfile;
|
|
20
|
+
avatar1?: string | null;
|
|
21
|
+
avatar2?: string | null;
|
|
22
|
+
}
|
|
23
|
+
/** Build an SVG compatibility comparison card */
|
|
24
|
+
export declare function buildCompatCardSvg(data: CompatCardData): string;
|
|
25
|
+
/** Render an SVG string to a PNG buffer */
|
|
26
|
+
export declare function svgToPng(svg: string): Promise<Buffer>;
|