anilink-api-wrapper 1.29.2 → 2.0.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 -21
- package/README.md +523 -241
- package/dist/AniLink.d.ts +11397 -0
- package/dist/AniLink.mjs +6664 -0
- package/dist/anilist.d.ts +1 -0
- package/dist/anilist.mjs +5 -0
- package/package.json +111 -46
- package/dist/AniLink.js +0 -248
- package/dist/apis/anilist/Custom.js +0 -42
- package/dist/apis/anilist/interfaces/Activity.js +0 -436
- package/dist/apis/anilist/interfaces/Basic.js +0 -33
- package/dist/apis/anilist/interfaces/CoverImage.js +0 -15
- package/dist/apis/anilist/interfaces/Distribution.js +0 -23
- package/dist/apis/anilist/interfaces/ExternalLink.js +0 -14
- package/dist/apis/anilist/interfaces/Favoured.js +0 -2
- package/dist/apis/anilist/interfaces/FuzzyDate.js +0 -12
- package/dist/apis/anilist/interfaces/Image.js +0 -13
- package/dist/apis/anilist/interfaces/ListScores.js +0 -2
- package/dist/apis/anilist/interfaces/Media.js +0 -13
- package/dist/apis/anilist/interfaces/Name.js +0 -15
- package/dist/apis/anilist/interfaces/NextAiringEpisode.js +0 -14
- package/dist/apis/anilist/interfaces/Notification.js +0 -25
- package/dist/apis/anilist/interfaces/Ranking.js +0 -19
- package/dist/apis/anilist/interfaces/SiteTrend.js +0 -33
- package/dist/apis/anilist/interfaces/Staff.js +0 -2
- package/dist/apis/anilist/interfaces/Stat.js +0 -2
- package/dist/apis/anilist/interfaces/Statistics.js +0 -2
- package/dist/apis/anilist/interfaces/StreamingEpisode.js +0 -15
- package/dist/apis/anilist/interfaces/Studio.js +0 -2
- package/dist/apis/anilist/interfaces/Tag.js +0 -18
- package/dist/apis/anilist/interfaces/Title.js +0 -15
- package/dist/apis/anilist/interfaces/Trailer.js +0 -14
- package/dist/apis/anilist/interfaces/UserStats.js +0 -23
- package/dist/apis/anilist/interfaces/responses/mutation/Favourites.js +0 -83
- package/dist/apis/anilist/interfaces/responses/query/AiringSchedule.js +0 -18
- package/dist/apis/anilist/interfaces/responses/query/AniChartUser.js +0 -2
- package/dist/apis/anilist/interfaces/responses/query/Character.js +0 -34
- package/dist/apis/anilist/interfaces/responses/query/ExternalLinkSourceCollection.js +0 -2
- package/dist/apis/anilist/interfaces/responses/query/Media.js +0 -217
- package/dist/apis/anilist/interfaces/responses/query/MediaList.js +0 -36
- package/dist/apis/anilist/interfaces/responses/query/MediaListCollectionResponse.js +0 -49
- package/dist/apis/anilist/interfaces/responses/query/MediaTagCollection.js +0 -2
- package/dist/apis/anilist/interfaces/responses/query/MediaTrend.js +0 -21
- package/dist/apis/anilist/interfaces/responses/query/Notification.js +0 -166
- package/dist/apis/anilist/interfaces/responses/query/Recommendation.js +0 -23
- package/dist/apis/anilist/interfaces/responses/query/Review.js +0 -30
- package/dist/apis/anilist/interfaces/responses/query/SiteStatistics.js +0 -31
- package/dist/apis/anilist/interfaces/responses/query/Staff.js +0 -59
- package/dist/apis/anilist/interfaces/responses/query/Studio.js +0 -60
- package/dist/apis/anilist/interfaces/responses/query/Thread.js +0 -44
- package/dist/apis/anilist/interfaces/responses/query/ThreadComment.js +0 -31
- package/dist/apis/anilist/interfaces/responses/query/User.js +0 -366
- package/dist/apis/anilist/mutation/DeleteActivity.js +0 -61
- package/dist/apis/anilist/mutation/DeleteActivityReply.js +0 -61
- package/dist/apis/anilist/mutation/DeleteCustomList.js +0 -63
- package/dist/apis/anilist/mutation/DeleteMediaListEntry.js +0 -61
- package/dist/apis/anilist/mutation/DeleteReview.js +0 -61
- package/dist/apis/anilist/mutation/DeleteThread.js +0 -61
- package/dist/apis/anilist/mutation/DeleteThreadComment.js +0 -61
- package/dist/apis/anilist/mutation/SaveActivityReply.js +0 -66
- package/dist/apis/anilist/mutation/SaveListActivity.js +0 -64
- package/dist/apis/anilist/mutation/SaveMediaListEntry.js +0 -97
- package/dist/apis/anilist/mutation/SaveMessageActivity.js +0 -68
- package/dist/apis/anilist/mutation/SaveRecommendation.js +0 -66
- package/dist/apis/anilist/mutation/SaveReview.js +0 -68
- package/dist/apis/anilist/mutation/SaveTextActivity.js +0 -65
- package/dist/apis/anilist/mutation/SaveThread.js +0 -69
- package/dist/apis/anilist/mutation/SaveThreadComment.js +0 -67
- package/dist/apis/anilist/mutation/ToggleActivityPin.js +0 -64
- package/dist/apis/anilist/mutation/ToggleActivitySubscription.js +0 -64
- package/dist/apis/anilist/mutation/ToggleFavourite.js +0 -66
- package/dist/apis/anilist/mutation/ToggleFollow.js +0 -62
- package/dist/apis/anilist/mutation/ToggleLike.js +0 -64
- package/dist/apis/anilist/mutation/ToggleLikeV2.js +0 -64
- package/dist/apis/anilist/mutation/ToggleThreadSubscription.js +0 -64
- package/dist/apis/anilist/mutation/UpdateAniChartHighlights.js +0 -58
- package/dist/apis/anilist/mutation/UpdateAniChartSettings.js +0 -58
- package/dist/apis/anilist/mutation/UpdateFavouriteOrder.js +0 -73
- package/dist/apis/anilist/mutation/UpdateMediaListEntries.js +0 -93
- package/dist/apis/anilist/mutation/UpdateUser.js +0 -127
- package/dist/apis/anilist/query/Activity.js +0 -86
- package/dist/apis/anilist/query/ActivityReply.js +0 -60
- package/dist/apis/anilist/query/AiringSchedule.js +0 -78
- package/dist/apis/anilist/query/AniChartUser.js +0 -56
- package/dist/apis/anilist/query/Character.js +0 -70
- package/dist/apis/anilist/query/ExternalLinkSourceCollection.js +0 -68
- package/dist/apis/anilist/query/Follower.js +0 -64
- package/dist/apis/anilist/query/Following.js +0 -64
- package/dist/apis/anilist/query/GenreCollection.js +0 -46
- package/dist/apis/anilist/query/Markdown.js +0 -57
- package/dist/apis/anilist/query/Media.js +0 -134
- package/dist/apis/anilist/query/MediaList.js +0 -90
- package/dist/apis/anilist/query/MediaListCollection.js +0 -76
- package/dist/apis/anilist/query/MediaTagCollection.js +0 -57
- package/dist/apis/anilist/query/MediaTrend.js +0 -84
- package/dist/apis/anilist/query/Notification.js +0 -65
- package/dist/apis/anilist/query/Recommendation.js +0 -68
- package/dist/apis/anilist/query/Review.js +0 -64
- package/dist/apis/anilist/query/SiteStatistics.js +0 -78
- package/dist/apis/anilist/query/Staff.js +0 -79
- package/dist/apis/anilist/query/Studio.js +0 -82
- package/dist/apis/anilist/query/Thread.js +0 -68
- package/dist/apis/anilist/query/ThreadComment.js +0 -63
- package/dist/apis/anilist/query/User.js +0 -68
- package/dist/apis/anilist/query/Viewer.js +0 -65
- package/dist/apis/anilist/query/page/Activities.js +0 -97
- package/dist/apis/anilist/query/page/ActivityReplies.js +0 -71
- package/dist/apis/anilist/query/page/AiringSchedules.js +0 -89
- package/dist/apis/anilist/query/page/Characters.js +0 -81
- package/dist/apis/anilist/query/page/Followers.js +0 -75
- package/dist/apis/anilist/query/page/Followings.js +0 -75
- package/dist/apis/anilist/query/page/Likes.js +0 -71
- package/dist/apis/anilist/query/page/MediaLists.js +0 -101
- package/dist/apis/anilist/query/page/MediaTrends.js +0 -95
- package/dist/apis/anilist/query/page/Medias.js +0 -143
- package/dist/apis/anilist/query/page/Notifications.js +0 -73
- package/dist/apis/anilist/query/page/Recommendations.js +0 -79
- package/dist/apis/anilist/query/page/Reviews.js +0 -75
- package/dist/apis/anilist/query/page/Staffs.js +0 -90
- package/dist/apis/anilist/query/page/Studios.js +0 -93
- package/dist/apis/anilist/query/page/ThreadCommments.js +0 -74
- package/dist/apis/anilist/query/page/Threads.js +0 -79
- package/dist/apis/anilist/query/page/Users.js +0 -79
- package/dist/apis/anilist/types/ActivityType.js +0 -14
- package/dist/apis/anilist/types/DisabledListActivity.js +0 -11
- package/dist/apis/anilist/types/Format.js +0 -30
- package/dist/apis/anilist/types/FuzzyDate.js +0 -12
- package/dist/apis/anilist/types/MediaListOptions.js +0 -16
- package/dist/apis/anilist/types/NotificationOptions.js +0 -29
- package/dist/apis/anilist/types/RecommendationRating.js +0 -12
- package/dist/apis/anilist/types/Season.js +0 -13
- package/dist/apis/anilist/types/Sort.js +0 -265
- package/dist/apis/anilist/types/Source.js +0 -25
- package/dist/apis/anilist/types/Status.js +0 -26
- package/dist/apis/anilist/types/Type.js +0 -44
- package/dist/apis/anilist/types/UserStaffNameLanguage.js +0 -15
- package/dist/apis/anilist/types/UserTitleLanguage.js +0 -15
- package/dist/base/APIWrapper.js +0 -17
- package/dist/base/RequestHandler.js +0 -48
- package/dist/base/ValidateVariables.js +0 -125
package/README.md
CHANGED
|
@@ -1,241 +1,523 @@
|
|
|
1
|
-
# AniLink
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
1
|
+
# AniLink
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/anilink-api-wrapper)
|
|
4
|
+
[](https://www.npmjs.com/package/anilink-api-wrapper)
|
|
5
|
+
[](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE)
|
|
6
|
+
[](https://github.com/RLAlpha49/AniLink/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/RLAlpha49/AniLink/actions/workflows/codeql.yml)
|
|
8
|
+
[](https://rlalpha49.github.io/AniLink/)
|
|
9
|
+
|
|
10
|
+
A typed TypeScript wrapper for the [AniList GraphQL API](https://docs.anilist.co/). AniLink turns raw AniList GraphQL into a set of named methods. You can query a user, save a list entry, or toggle a favourite. You do not need to write query strings or hand-roll HTTP.
|
|
11
|
+
|
|
12
|
+
> 🧪 Try the [AniLink API Explorer](https://rlalpha49.github.io/AniLink/explorer/) — build and test AniLink calls live against AniList.
|
|
13
|
+
|
|
14
|
+
## Why It Exists
|
|
15
|
+
|
|
16
|
+
AniList exposes a GraphQL API that is flexible but verbose. Every request needs a query document, a variables object, and careful handling of the response shape. AniLink removes that ceremony. You call a method, pass a plain object, and get back typed data. AniLink also validates your variables before they leave your app. A wrong field type fails fast with a clear message instead of a confusing API error.
|
|
17
|
+
|
|
18
|
+
## What You Can Do With It
|
|
19
|
+
|
|
20
|
+
AniLink uses one instance with a single `anilist` surface:
|
|
21
|
+
|
|
22
|
+
- `anilist.query` fetches data. This includes users, media, characters, staff, studios, reviews, activities, threads, notifications, and more.
|
|
23
|
+
|
|
24
|
+
- `anilist.query.page` returns paginated versions of the same resources.
|
|
25
|
+
|
|
26
|
+
- `anilist.paginate`, `anilist.paginatePages`, and `anilist.paginateChunks` walk every page or chunk for you, so you do not hand-roll `hasNextPage` loops.
|
|
27
|
+
|
|
28
|
+
- `anilist.mutation` changes data. You can update your profile, save and delete list entries, and post activities and replies. You can also toggle likes and favourites and manage reviews, threads, and AniChart settings.
|
|
29
|
+
|
|
30
|
+
- `anilist.custom` sends any raw query or mutation when you need something the named methods do not cover.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { AniLink } from "anilink-api-wrapper";
|
|
34
|
+
|
|
35
|
+
// Remember that you can create multiple instances with different auth tokens
|
|
36
|
+
// or one instance without a token for public queries.
|
|
37
|
+
const aniLink = new AniLink();
|
|
38
|
+
const aniLinkAuth = new AniLink("your-auth-token");
|
|
39
|
+
|
|
40
|
+
// Fetch a user
|
|
41
|
+
const user = await aniLink.anilist.query.user({ id: 542244 });
|
|
42
|
+
|
|
43
|
+
// Save an anime to your list
|
|
44
|
+
await aniLinkAuth.anilist.mutation.saveMediaListEntry({
|
|
45
|
+
mediaId: 1,
|
|
46
|
+
status: "COMPLETED",
|
|
47
|
+
score: 9,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Send a raw query
|
|
51
|
+
const viewer = await aniLinkAuth.anilist.custom("query { Viewer { id } }");
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Key Features
|
|
55
|
+
|
|
56
|
+
- **Typed end to end.** Every method has a typed variables interface and a typed response. Your editor catches mistakes before runtime.
|
|
57
|
+
|
|
58
|
+
- **No GraphQL strings for common tasks.** The named query and mutation methods cover the everyday AniList operations.
|
|
59
|
+
|
|
60
|
+
- **Variable validation.** AniLink checks required fields and types before sending. It throws a descriptive error like `Invalid id: 542244. Expected type: number` when something is wrong.
|
|
61
|
+
|
|
62
|
+
- **Optional auth.** Construct with a token for authenticated calls, or without one for public queries. You can create multiple instances with different tokens.
|
|
63
|
+
|
|
64
|
+
- **Custom escape hatch.** `anilist.custom` accepts any query or mutation string with an optional variables object.
|
|
65
|
+
|
|
66
|
+
- **Pagination built in.** Page queries accept `page` and `perPage`, and the `paginate` / `paginatePages` / `paginateChunks` helpers walk every page or chunk with a max-page guard.
|
|
67
|
+
|
|
68
|
+
- **Clear error handling.** API errors, including rate limits, surface as thrown errors you can catch and retry.
|
|
69
|
+
|
|
70
|
+
## Who It Is For
|
|
71
|
+
|
|
72
|
+
AniLink is for developers building tools around AniList. This includes trackers, recommendation engines, Discord bots, dashboards, or anything that reads or writes AniList data. If you would rather call `query.media({ id: 1 })` than maintain GraphQL documents, this is for you.
|
|
73
|
+
|
|
74
|
+
## Getting Started
|
|
75
|
+
|
|
76
|
+
### Install
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm install anilink-api-wrapper
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Initialize
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { AniLink } from "anilink-api-wrapper";
|
|
86
|
+
|
|
87
|
+
// With a token (required for authenticated queries and mutations)
|
|
88
|
+
const aniLinkAuth = new AniLink("your-auth-token");
|
|
89
|
+
|
|
90
|
+
// Without a token (public queries only)
|
|
91
|
+
const aniLink = new AniLink();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Get a token by registering an application on the [AniList developer settings](https://anilist.co/settings/developer).
|
|
95
|
+
|
|
96
|
+
#### Client options
|
|
97
|
+
|
|
98
|
+
The second constructor argument accepts optional transport settings. Options are **per instance**: creating a second `AniLink` never changes how an existing one behaves.
|
|
99
|
+
|
|
100
|
+
| Option | Type | Default | Description |
|
|
101
|
+
| --------------------- | ------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `timeout` | `number` | `30000` | Milliseconds before a request is aborted. `0` disables the timeout. Timeout errors carry the effective duration as `timeoutMs`. |
|
|
103
|
+
| `signal` | `AbortSignal` | — | Cancel in-flight requests (for example when a user navigates away). |
|
|
104
|
+
| `retry` | `boolean \| Partial<RetryPolicy>` | default policy | Automatic retries for transient failures are **on by default** (`maxRetries: 3` over HTTP `429`/`5xx`, network, and timeout errors). Pass `retry: false` to opt out. See [Retry with backoff](#retry-with-backoff). |
|
|
105
|
+
| `paceWithRateLimit` | `boolean` | `false` | Opt into proactive pacing: when the `x-ratelimit-remaining` header drops below `rateLimitFloor`, the next request waits until the window resets instead of discovering the limit via a `429`. |
|
|
106
|
+
| `rateLimitFloor` | `number` | `1` | Remaining-quota threshold below which `paceWithRateLimit` delays the next request. |
|
|
107
|
+
| `circuitBreaker` | `{ threshold: number; cooldownMs: number }` | — | Opt into a per-client circuit breaker: after `threshold` consecutive failures, requests fail fast with `CIRCUIT_OPEN_ERROR` until `cooldownMs` elapses. |
|
|
108
|
+
| `onError` | `(error, context) => void` | — | Invoked when an attempt fails and once more when retries are exhausted. |
|
|
109
|
+
| `onRetry` | `(error, context) => void` | — | Invoked before each retry wait with the scheduled delay. |
|
|
110
|
+
| `onRequestStart` | `(context) => void` | — | Invoked just before each attempt is sent. |
|
|
111
|
+
| `onResponse` | `(context) => void` | — | Invoked after each attempt completes, with the elapsed `durationMs`. |
|
|
112
|
+
| `exposeRawAxiosError` | `boolean` | `false` | Attach the original Axios error to thrown errors for local debugging. |
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
const aniLink = new AniLink("your-auth-token", {
|
|
116
|
+
timeout: 10_000,
|
|
117
|
+
retry: { maxRetries: 2 },
|
|
118
|
+
onResponse: ({ url, durationMs }) => console.log(url, `${durationMs}ms`),
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The hook context carries `url`, `method`, the 1-based `attempt`, the stable error `code`, the HTTP `status` for API failures, and `nextDelayMs` when the failure will be retried.
|
|
123
|
+
|
|
124
|
+
### OAuth
|
|
125
|
+
|
|
126
|
+
AniLink ships helpers for the AniList OAuth2 authorization-code flow, so you can obtain and refresh the token you pass to the `AniLink` constructor instead of hand-rolling the HTTP calls.
|
|
127
|
+
|
|
128
|
+
First, register an application on the [AniList developer settings](https://anilist.co/settings/developer) to get a client ID and client secret, and pick a redirect URI.
|
|
129
|
+
|
|
130
|
+
Send the user to the authorization URL to approve your application:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { buildAuthorizationUrl } from "anilink-api-wrapper";
|
|
134
|
+
|
|
135
|
+
const state = crypto.randomUUID(); // a fresh random value per login attempt
|
|
136
|
+
const authorizeUrl = buildAuthorizationUrl("your-client-id", "https://example.com/callback", state);
|
|
137
|
+
// Redirect the user to `authorizeUrl`. After approval, AniList sends them back
|
|
138
|
+
// to your redirect URI with `?code=` and `state=` query parameters.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The third `state` parameter is **optional** but strongly recommended as additional CSRF protection. Always pass a random `state`, bind it to the user's session, and validate that the `state` on the redirect matches before exchanging the code. This cross-site request forgery (CSRF) check stops attackers from completing authorization flows your users never started.
|
|
142
|
+
|
|
143
|
+
Exchange the authorization code from the redirect for an access token:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { getAccessToken, AniLink } from "anilink-api-wrapper";
|
|
147
|
+
|
|
148
|
+
const { access_token, refresh_token } = await getAccessToken(
|
|
149
|
+
"your-client-id",
|
|
150
|
+
"your-client-secret",
|
|
151
|
+
code, // the `code` query parameter from the redirect
|
|
152
|
+
"https://example.com/callback"
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const aniLink = new AniLink(access_token);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
When the access token expires, exchange the stored refresh token for a new one. Note that the refresh response may not include a new `refresh_token`, in which case you keep using the one you stored:
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { refreshAccessToken } from "anilink-api-wrapper";
|
|
162
|
+
|
|
163
|
+
const { access_token, refresh_token: rotated } = await refreshAccessToken(
|
|
164
|
+
"your-client-id",
|
|
165
|
+
"your-client-secret",
|
|
166
|
+
refresh_token
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const nextRefreshToken = rotated ?? refresh_token;
|
|
170
|
+
const aniLink = new AniLink(access_token);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
AniList reports the token lifetime as `expires_in` seconds. Use `getTokenExpiry` to refresh proactively before the token expires instead of waiting for a `401`:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { getTokenExpiry, refreshAccessToken } from "anilink-api-wrapper";
|
|
177
|
+
|
|
178
|
+
if (Date.now() >= getTokenExpiry(tokenResponse).getTime() - 60_000) {
|
|
179
|
+
// Refresh at least a minute before expiry.
|
|
180
|
+
tokenResponse = await refreshAccessToken(
|
|
181
|
+
"your-client-id",
|
|
182
|
+
"your-client-secret",
|
|
183
|
+
nextRefreshToken
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Query
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
const user = await aniLink.anilist.query.user({ id: 542244, asHtml: true });
|
|
192
|
+
const media = await aniLink.anilist.query.media({ id: 1, type: "ANIME" });
|
|
193
|
+
const viewer = await aniLink.anilist.query.viewer({ asHtml: true });
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Paginate
|
|
197
|
+
|
|
198
|
+
Page queries accept `page` and `perPage` and return a single page with `pageInfo`. Fetch one page when you know the range:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const page = await aniLink.anilist.query.page.medias({
|
|
202
|
+
page: 1,
|
|
203
|
+
perPage: 10,
|
|
204
|
+
type: "ANIME",
|
|
205
|
+
sort: ["POPULARITY_DESC"],
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
To walk every page, use the helpers on `aniLink.anilist`. They track `page`/`perPage` and `hasNextPage` for you and stop at a `maxPages` guard, so a runaway loop cannot fetch forever.
|
|
210
|
+
|
|
211
|
+
`paginate` collects every item across all pages into one array:
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
const result = await aniLink.anilist.paginate(
|
|
215
|
+
(page, perPage) => aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" }),
|
|
216
|
+
"media",
|
|
217
|
+
{ perPage: 50, maxPages: 10 }
|
|
218
|
+
);
|
|
219
|
+
console.log(result.items.length, result.pageCount, result.truncated);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`paginatePages` yields each raw page response in turn. Use it for streaming or early exit when you do not need every item in memory:
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
for await (const page of aniLink.anilist.paginatePages((page, perPage) =>
|
|
226
|
+
aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" })
|
|
227
|
+
)) {
|
|
228
|
+
console.log(page.pageInfo.currentPage, page.media.length);
|
|
229
|
+
if (page.media.length > 0 && page.media[0].id === 1) break;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
`paginateChunks` walks `MediaListCollection` chunks, which AniList returns with `hasNextChunk` instead of `pageInfo`:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const result = await aniLink.anilist.paginateChunks(
|
|
237
|
+
(chunk, perChunk) =>
|
|
238
|
+
aniLink.anilist.query.mediaListCollection({
|
|
239
|
+
userId: 542244,
|
|
240
|
+
type: "ANIME",
|
|
241
|
+
chunk,
|
|
242
|
+
perChunk,
|
|
243
|
+
}),
|
|
244
|
+
"lists",
|
|
245
|
+
{ perChunk: 500, maxChunks: 20 }
|
|
246
|
+
);
|
|
247
|
+
console.log(result.items.length, result.chunkCount, result.truncated);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Each helper returns `truncated: true` when it stopped at the guard before the source ran out of pages or chunks.
|
|
251
|
+
|
|
252
|
+
By default pages and chunks are fetched strictly one at a time. Pass `concurrency` to keep several requests in flight while collecting results — useful for large traversals where per-page latency dominates. Results are always returned in order regardless of which request finishes first, scheduling stops as soon as the source reports no more data, and the `maxPages` / `maxChunks` guards still apply:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const result = await aniLink.anilist.paginate(
|
|
256
|
+
(page, perPage) => aniLink.anilist.query.page.medias({ page, perPage, type: "ANIME" }),
|
|
257
|
+
"media",
|
|
258
|
+
{ perPage: 50, maxPages: 10, concurrency: 4 }
|
|
259
|
+
);
|
|
260
|
+
console.log(result.items.length, result.pageCount, result.truncated);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Keep `concurrency` modest (2–8): AniList rate-limits aggressive clients, and values above 8 are clamped down to 8.
|
|
264
|
+
|
|
265
|
+
### Mutate
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
await aniLink.anilist.mutation.saveMediaListEntry({
|
|
269
|
+
mediaId: 1,
|
|
270
|
+
status: "COMPLETED",
|
|
271
|
+
score: 9,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
await aniLink.anilist.mutation.toggleFavourite({ animeId: 1 });
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Handle errors
|
|
278
|
+
|
|
279
|
+
AniLink throws typed errors with stable `code` values. HTTP failures are
|
|
280
|
+
represented by `AniLinkApiError` and expose the HTTP `status`; network,
|
|
281
|
+
timeout, and cancellation failures use `AniLinkNetworkError`. Calling an
|
|
282
|
+
authenticated operation without a token throws `AniLinkAuthError`. Successful
|
|
283
|
+
AniList response data is returned normally as the typed result of each
|
|
284
|
+
query or mutation. For failed API requests, `AniLinkApiError.data` contains
|
|
285
|
+
the upstream AniList response body. AniLink does not expose the raw Axios
|
|
286
|
+
response object, request headers, bearer token, or request internals.
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import {
|
|
290
|
+
AniLinkApiError,
|
|
291
|
+
AniLinkAuthError,
|
|
292
|
+
AniLinkGraphQLError,
|
|
293
|
+
AniLinkNetworkError,
|
|
294
|
+
} from "anilink-api-wrapper";
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const user = await aniLink.anilist.query.user({ id: 542244 });
|
|
298
|
+
console.log(user);
|
|
299
|
+
} catch (error: unknown) {
|
|
300
|
+
if (error instanceof AniLinkApiError) {
|
|
301
|
+
console.error(error.code, error.status, error.data);
|
|
302
|
+
|
|
303
|
+
if (error.status === 429) {
|
|
304
|
+
// Rate-limit accounting from the response headers, when present.
|
|
305
|
+
console.error("Quota reset at:", error.rateLimit?.reset);
|
|
306
|
+
}
|
|
307
|
+
} else if (error instanceof AniLinkGraphQLError) {
|
|
308
|
+
// The request returned HTTP 200 but carried GraphQL errors.
|
|
309
|
+
console.error(error.graphqlErrors.map((e) => e.message));
|
|
310
|
+
console.error(error.data); // any partial data returned alongside the errors
|
|
311
|
+
} else if (error instanceof AniLinkAuthError) {
|
|
312
|
+
console.error(error.code, error.message);
|
|
313
|
+
} else if (error instanceof AniLinkNetworkError) {
|
|
314
|
+
console.error(error.code, error.message);
|
|
315
|
+
} else {
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
The available transport codes are `API_ERROR`, `GRAPHQL_ERROR`, `NETWORK_ERROR`,
|
|
322
|
+
`TIMEOUT_ERROR`, `ABORTED_ERROR`, `CIRCUIT_OPEN_ERROR`, `AUTH_ERROR`,
|
|
323
|
+
`VALIDATION_ERROR`, and `UNKNOWN_ERROR`.
|
|
324
|
+
|
|
325
|
+
GraphQL-level failures can arrive inside an HTTP 200 response. AniLink throws
|
|
326
|
+
these as `AniLinkGraphQLError` (a subclass of `AniLinkApiError` with `status`
|
|
327
|
+
`200`) instead of returning half-valid data. The upstream `errors` array is
|
|
328
|
+
available on `error.graphqlErrors`, and any partial `data` on `error.data`.
|
|
329
|
+
|
|
330
|
+
When AniList includes rate-limit headers (`x-ratelimit-limit`,
|
|
331
|
+
`x-ratelimit-remaining`, `x-ratelimit-reset`), every `AniLinkApiError` exposes
|
|
332
|
+
them as a read-only `rateLimit` object so schedulers and UIs can self-throttle.
|
|
333
|
+
|
|
334
|
+
For local debugging, you can opt into the original Axios error:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
const debugClient = new AniLink("your-auth-token", {
|
|
338
|
+
exposeRawAxiosError: true,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
await debugClient.anilist.query.user({ id: 542244 });
|
|
343
|
+
} catch (error: unknown) {
|
|
344
|
+
if (error instanceof AniLinkApiError) {
|
|
345
|
+
console.error(error.rawAxiosError);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`exposeRawAxiosError` defaults to `false`. Use it for local debugging;
|
|
351
|
+
the raw Axios error can contain request configuration and bearer-token
|
|
352
|
+
headers.
|
|
353
|
+
|
|
354
|
+
### Retry with backoff
|
|
355
|
+
|
|
356
|
+
AniLink retries transient failures for you by default: HTTP `429` and `5xx`
|
|
357
|
+
responses plus network and timeout errors are retried with exponential
|
|
358
|
+
backoff (`maxRetries: 3`). The `Retry-After` header is honored for `429`
|
|
359
|
+
responses (capped at 60 seconds). Pass `retry: false` to opt out and send
|
|
360
|
+
every request exactly once, or pass a partial policy to tune individual
|
|
361
|
+
knobs on top of the defaults.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
// Opt out of the default policy
|
|
365
|
+
const aniLink = new AniLink("your-auth-token", { retry: false });
|
|
366
|
+
|
|
367
|
+
// Or tune the default policy
|
|
368
|
+
const aniLink = new AniLink("your-auth-token", {
|
|
369
|
+
retry: {
|
|
370
|
+
maxRetries: 3, // retries after the initial attempt
|
|
371
|
+
baseDelayMs: 250, // first backoff delay
|
|
372
|
+
maxDelayMs: 5_000, // backoff cap
|
|
373
|
+
retryOnStatus: [429, 500, 502, 503, 504],
|
|
374
|
+
retryOnNetworkError: true,
|
|
375
|
+
jitter: true, // randomize each wait within [0, computed delay]
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Backoff delays use **full jitter** by default: each wait is a random value
|
|
381
|
+
between `0` and the computed exponential cap. This spreads out retries from
|
|
382
|
+
many concurrent clients instead of letting them re-fire in lockstep against
|
|
383
|
+
the shared rate limit. Server-dictated `Retry-After` waits are never jittered.
|
|
384
|
+
Pass `jitter: false` for deterministic delays.
|
|
385
|
+
|
|
386
|
+
### Rate-limit pacing and circuit breaking
|
|
387
|
+
|
|
388
|
+
Two further resilience knobs are available per instance, both off by default:
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { AniLinkNetworkError } from "anilink-api-wrapper";
|
|
392
|
+
|
|
393
|
+
const aniLink = new AniLink("your-auth-token", {
|
|
394
|
+
// Slow down before AniList does: once a successful response reports fewer
|
|
395
|
+
// remaining requests than `rateLimitFloor`, hold the next request until
|
|
396
|
+
// the window resets instead of eating a 429.
|
|
397
|
+
paceWithRateLimit: true,
|
|
398
|
+
rateLimitFloor: 5,
|
|
399
|
+
// Fail fast during sustained outages: after 5 consecutive failures,
|
|
400
|
+
// requests throw `CIRCUIT_OPEN_ERROR` without touching the network for
|
|
401
|
+
// 30 seconds, then one probe request is let through again.
|
|
402
|
+
circuitBreaker: { threshold: 5, cooldownMs: 30_000 },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
await aniLink.anilist.query.user({ id: 542244 });
|
|
407
|
+
} catch (error: unknown) {
|
|
408
|
+
if (error instanceof AniLinkNetworkError && error.code === "CIRCUIT_OPEN_ERROR") {
|
|
409
|
+
// Back off; the breaker will probe recovery automatically.
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Error hook
|
|
415
|
+
|
|
416
|
+
The `onError` hook is invoked when a request ultimately fails after all
|
|
417
|
+
retries are exhausted (or immediately when retries are disabled). Use it
|
|
418
|
+
to implement your own fallback (for example a cache or an offline queue).
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { AniLink, AniLinkApiError } from "anilink-api-wrapper";
|
|
422
|
+
|
|
423
|
+
const aniLink = new AniLink("your-auth-token", {
|
|
424
|
+
onError: (error, context) => {
|
|
425
|
+
console.error(`Request to ${context.url} failed on attempt ${context.attempt}`);
|
|
426
|
+
if (error instanceof AniLinkApiError && error.status === 429) {
|
|
427
|
+
// queue the request for later
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Observability hooks
|
|
434
|
+
|
|
435
|
+
Beyond `onError`, three optional hooks let you instrument the request
|
|
436
|
+
lifecycle without wrapping any methods. All are per-instance options:
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
const aniLink = new AniLink("your-auth-token", {
|
|
440
|
+
// Fires just before each attempt is sent.
|
|
441
|
+
onRequestStart: ({ url, method, attempt }) => {
|
|
442
|
+
console.log(`#${attempt} -> ${method} ${url}`);
|
|
443
|
+
},
|
|
444
|
+
// Fires after each attempt completes, success or failure.
|
|
445
|
+
onResponse: ({ url, durationMs }) => {
|
|
446
|
+
console.log(`${url} took ${durationMs}ms`);
|
|
447
|
+
},
|
|
448
|
+
// Fires before each retry wait, with the scheduled delay.
|
|
449
|
+
onRetry: (error, { attempt, nextDelayMs, status }) => {
|
|
450
|
+
console.warn(
|
|
451
|
+
`attempt ${attempt} failed (${status ?? error.code}); retrying in ${nextDelayMs}ms`
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
- `onRequestStart` receives `{ url, method, attempt }`.
|
|
458
|
+
- `onResponse` receives the same context plus `durationMs`, and fires for
|
|
459
|
+
failed attempts too, so it can drive latency histograms.
|
|
460
|
+
- `onRetry` receives the normalized error plus `{ url, method, attempt, code,
|
|
461
|
+
status?, nextDelayMs }`. When you do not set `onRetry`, per-attempt
|
|
462
|
+
notifications fall back to `onError` (which then fires before each retry
|
|
463
|
+
wait and once more at terminal failure).
|
|
464
|
+
|
|
465
|
+
These hooks are synchronous and fire in addition to the promise result; they
|
|
466
|
+
never change request behavior. Hook exceptions are isolated: a throwing hook
|
|
467
|
+
is reported as a console warning and never crashes a request or distorts
|
|
468
|
+
retry and error classification.
|
|
469
|
+
|
|
470
|
+
Common status codes from the AniList API:
|
|
471
|
+
|
|
472
|
+
- `400` bad request
|
|
473
|
+
- `401` unauthorized
|
|
474
|
+
- `404` not found
|
|
475
|
+
- `429` too many requests (rate limit)
|
|
476
|
+
- `500` internal server error
|
|
477
|
+
|
|
478
|
+
## Documentation
|
|
479
|
+
|
|
480
|
+
Full method and parameter reference: [AniLink documentation](https://rlalpha49.github.io/AniLink/).
|
|
481
|
+
|
|
482
|
+
More usage examples: [ANILIST_API_EXAMPLES](https://github.com/RLAlpha49/AniLink/blob/master/Examples/ANILIST_API_EXAMPLES.md).
|
|
483
|
+
|
|
484
|
+
Coverage versus the current AniList schema — which upstream operations AniLink wraps, which it deliberately does not, and any detected drift — is rendered into [`artifacts/anilist-api-compare/report.md`](artifacts/anilist-api-compare/report.md) by every CI run and kept as a retained workflow artifact.
|
|
485
|
+
|
|
486
|
+
### Upstream compatibility
|
|
487
|
+
|
|
488
|
+
AniList's GraphQL API is unversioned: fields can appear, be deprecated, or disappear at any time. AniLink pins a schema snapshot and gates every pull request against it with `npm run anilist:api:compare -- --strict --ignore-unimplemented`, so drift is caught before merge rather than at your runtime. The comparison report classifies upstream changes as:
|
|
489
|
+
|
|
490
|
+
- **Discrepancy** — an implemented operation's contract no longer matches the snapshot (missing field, changed type). Strict mode fails CI; these ship as a patch or minor fix-forward.
|
|
491
|
+
- **Removed operation** — an operation AniLink wrapped no longer exists upstream. The wrapper method is dropped and the removal ships as a **minor** release with a documented migration snippet using `custom()`.
|
|
492
|
+
- **Deprecated operation** — AniList marked a field or operation deprecated. AniLink keeps wrapping it, tracks the deprecation in the report, and removes it only when AniList does.
|
|
493
|
+
- **Unimplemented operation** — an upstream operation AniLink has never wrapped. These are listed in the report; operations that can never be wrapped are recorded with a dated review note in `IGNORED_UNIMPLEMENTED_OPERATIONS` (`lib/api-compare/compare.ts`).
|
|
494
|
+
|
|
495
|
+
Until a removal ships, `custom()` lets you call any upstream operation directly:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
const result = await aniLink.anilist.custom(
|
|
499
|
+
"query ($id: Int) { Media (id: $id) { id title { romaji } } }",
|
|
500
|
+
{ id: 1 }
|
|
501
|
+
);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Development
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
npm install # install dependencies
|
|
508
|
+
npm run typecheck # TypeScript type checking
|
|
509
|
+
npm test # run unit tests
|
|
510
|
+
npm run test:integration # run integration tests
|
|
511
|
+
npm run lint # lint source and tests
|
|
512
|
+
npm run build # build to dist/
|
|
513
|
+
npm run docs:generate # generate API docs to docs/
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Resources
|
|
517
|
+
|
|
518
|
+
- [Contributing guide](CONTRIBUTING.md)
|
|
519
|
+
- [Changelog](CHANGELOG.md) and [GitHub Releases](https://github.com/RLAlpha49/AniLink/releases)
|
|
520
|
+
|
|
521
|
+
## License
|
|
522
|
+
|
|
523
|
+
[MIT](https://github.com/RLAlpha49/AniLink/blob/master/LICENSE)
|