dotnet add package AniAPI.NET
&state=
66 | ```
67 |
68 | As you can see, the **querystring** contains the generated `authorization_code` and the optional `state` value you provided initially.
69 |
70 | :::warning
71 |
72 | The `authorization_code` validity time is of `3` minutes since creation.
73 |
74 | :::
75 |
76 | ## Exchange the authorization code with the token
77 |
78 | In order to obtain the user's `access_token`, you need to make a `POST` request to `https://api.aniapi.com/v1/oauth/token` with the following parameters:
79 |
80 | | Name | Required | Description |
81 | | --- | --- | --- |
82 | | `client_id` | Yes | Your client ID |
83 | | `client_secret` | Yes | Your client secret |
84 | | `code` | Yes | The `authorization_code` you got earlier |
85 | | `redirect_uri` | Yes | Your client redirect URI |
86 |
87 | ```bash title="Example request"
88 | curl -i -X POST https://api.aniapi.com/v1/oauth/token?
89 | client_id=
90 | &client_secret=
91 | &code=
92 | &redirect_uri=
93 | ```
94 |
95 | If all goes right, you should receive a **JSON-Encoded** response with the user's `access_token`:
96 |
97 | ```js title="Example response"
98 | {
99 | "status": 200,
100 | "message": "Code verified",
101 | "data": "",
102 | "version": "1"
103 | }
104 | ```
105 |
--------------------------------------------------------------------------------
/docs/oauth/implicit_grant.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Implicit Grant
6 |
7 | As you can't store **your client's credentials** in a safe area, you'll need to use the implicit grant.
8 | This way is mainly used by **frontend-only** environments, like websites or mobile applications.
9 |
10 | ## How it works
11 |
12 | 1. The user asks for **Login** inside your application
13 | 2. Instead of building a *login-form*, you just redirect the user to **our OAuth endpoint**
14 | 3. The user authenticates with their credentials
15 | 4. If nothing goes wrong, the AniAPI server redirects the user to your app with their `access_token`
16 |
17 | ## Requirements
18 |
19 | * An AniAPI OAuth Client
20 | * A website or a mobile application
21 | * A temporary webserver to grab the token
22 |
23 | ## Redirect the user
24 |
25 | Open a browser window (or just redirect if your application is a website) and make a request to `https://api.aniapi.com/v1/oauth`.
26 | The `oauth` endpoint expects some parameters to identify the client calling it:
27 |
28 | | Name | Required | Description |
29 | | --- | --- | --- |
30 | | `client_id` | Yes | Your client ID |
31 | | `redirect_uri` | Yes | Your client redirect URI |
32 | | `response_type` | Yes | For implicit grant pass `token` |
33 | | `state` | No | A random string generated by your application |
34 |
35 | :::info
36 |
37 | The `client_id` and `redirect_uri` values must match your client's one.
38 |
39 | :::
40 |
41 | The `state` parameter (optional) is used to protect your application from *cross-site request forgery* (**CSRF**).
42 | If provided, the AniAPI server will return it alongside the user's `access_token`.
43 | Verify it against the value you provided before to validate the response.
44 |
45 | ```http title="Example request URL"
46 | https://api.aniapi.com/v1/oauth?
47 | response_type=token
48 | &client_id=
49 | &redirect_uri=
50 | &state=
51 | ```
52 |
53 | ## Retrieve the token
54 |
55 | Once the user approved the **Authentication** request and completed the login step, the **AniAPI server** will redirect them back to your application.
56 | Let's assume you provided `http://localhost:3000/auth` as `redirect_uri` value. This will be the redirection URL:
57 |
58 | ```http title="Example redirect URL"
59 | http://localhost:3000/auth/#access_token=&state=
60 | ```
61 |
62 | As you can see, inside the **URL fragment** there will be the user's `access_token` and the optional `state` value you provided initially.
63 |
64 | :::caution
65 |
66 | The **URL fragment** differs from a **querystring** because it is accesible from **frontend** only.
67 |
68 | You need to extract it by using **[JavaScript](https://en.wikipedia.org/wiki/JavaScript)** inside the webpage.
69 |
70 | :::
71 |
--------------------------------------------------------------------------------
/docs/pagination.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | ---
4 |
5 | # Pagination
6 |
7 | All API resources have support for **bulk fetches** via *list* API methods.
8 | These methods share a common structure, which allows six additional parameters:
9 |
10 | | Name | Default | Description |
11 | | --- | --- | --- |
12 | | `locale` | `en` | Indicates the resource's desired localization |
13 | | `page` | `1` | Indicates the list page to fetch |
14 | | `per_page` | `100` | Indicates the desired results number on each page (max value is `100`) |
15 | | `ids` | `[]` | An array filter you can apply on the `id` field of results |
16 | | `sort_fields` | `[]` | An array of fields to apply a specific sort on the results |
17 | | `sort_directions` | `[]` | An array of integers (`1` or `-1`) to specify the sort direction on each field in `sort_fields` |
18 |
19 | :::info
20 |
21 | Actually `locale` parameter is used on few resource types
22 |
23 | :::
24 |
25 | :::caution
26 |
27 | When sorting, if you did not provide a `direction` for a `field`, **AniAPI** will use `1` as default
28 |
29 | :::
30 |
31 | ## Resources structure
32 |
33 | Every *Resource* of **AniAPI** comes with a default set of fields:
34 |
35 | | Name | Description |
36 | | --- | --- |
37 | | `creation_date` | The document's creation datetime |
38 | | `update_date` | The document's last update datetime |
39 |
40 | :::caution
41 |
42 | These fields are not returned from API fetches.
43 | They are just used for sorting scopes.
44 |
45 | :::
46 |
47 | ## Paginated response
48 |
49 | In order to give informations about the returned list of resources, AniAPI also provides **pagination parameters** inside the response's `data` field:
50 |
51 | * `current_page`, which tells you the current page number
52 | * `count`, indicating the number of total documents satisfying the filter
53 | * `documents`, an array containing the current page documents
54 | * `last_page`, to indicate the last page available to fetch
55 |
56 | ```js title="Example paginated response"
57 | {
58 | "status_code": 200,
59 | "message": "Page 1 contains 100 anime. Last page number is 161 for a total of 16094 anime",
60 | "data": {
61 | "current_page": 1,
62 | "count": 16094,
63 | "documents": [
64 | {
65 | // ...
66 | },
67 | {
68 | // ...
69 | },
70 | // ...
71 | ],
72 | "last_page": 161
73 | }
74 | }
75 | ```
76 |
--------------------------------------------------------------------------------
/docs/rate_limiting.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | # Rate Limiting
6 |
7 | As AniAPI is free-to-use, we wanna be sure to avoid too many requests within a limited span of time.
8 | Therefore, request from the same origin are limited to `90` per **minute**.
9 |
10 | Inside each **HTTP response header** from the API, you will receive three additional **headers**:
11 |
12 | * `X-RateLimit-Limit`, which indicates the **maximum requests number** you can do in a minute
13 | * `X-RateLimit-Remaining`, to update you on your **current remaining number of requests**
14 | * `X-RateLimit-Reset`, which tells you about **how many seconds you have to wait** until you can make another **accepted request**
15 |
16 | :::tip
17 |
18 | You have to care about `X-RateLimit-Reset` only if you do more than `90` requests in a minute
19 |
20 | :::
21 |
22 | When you go over `90` requests in a minute, you will get a `429` **HTTP error** as response.
23 |
--------------------------------------------------------------------------------
/docs/resources/_category_.json:
--------------------------------------------------------------------------------
1 | {
2 | "label": "Resources",
3 | "position": 8
4 | }
--------------------------------------------------------------------------------
/docs/resources/anime.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 1
3 | ---
4 |
5 | # Anime
6 |
7 | import ApiCodeBlock from '../../src/components/ApiCodeBlock';
8 | import Highlight from '../../src/components/Highlight';
9 | import ApiTryIt from '../../src/components/ApiTryIt';
10 |
11 | export const endpoints = [
12 | { method: 'GET', uri: '/v1/anime/:id' },
13 | { method: 'GET', uri: '/v1/anime' },
14 | { method: 'GET', uri: '/v1/random/anime/:count/:nsfw' }
15 | ];
16 |
17 | This is an object representing an Anime show.
18 | The API allows you to retrieve individual Anime as well as a list of them using various filters.
19 |
20 |
21 |
22 | ## The Anime object
23 |
24 | ### Attributes
25 |
26 | ---
27 |
28 | #### id
29 |
30 | Unique identifier for an Anime.
31 |
32 | ---
33 |
34 | #### anilist_id
35 |
36 | ***[AniList](https://anilist.co/)*** external unique identifier.
37 |
38 | ---
39 |
40 | #### mal_id
41 |
42 | ***[MyAnimeList](https://myanimelist.net/)*** external unique identifier.
43 |
44 | ---
45 |
46 | #### tmdb_id
47 |
48 | ***[TheMovieDatabase](https://www.themoviedb.org/)*** external unique identifier.
49 |
50 | ---
51 |
52 | #### format
53 |
54 | The show's format destination.
55 |
56 | ```js title="Possible format enum values"
57 | "TV": 0,
58 | "TV_SHORT": 1,
59 | "MOVIE": 2,
60 | "SPECIAL": 3,
61 | "OVA": 4,
62 | "ONA": 5,
63 | "MUSIC": 6
64 | ```
65 |
66 | ---
67 |
68 | #### status
69 |
70 | The show's global release status.
71 |
72 | ```js title="Possible status enum values"
73 | "FINISHED": 0,
74 | "RELEASING": 1,
75 | "NOT_YET_RELEASED": 2,
76 | "CANCELLED": 3
77 | ```
78 |
79 | ---
80 |
81 | #### titles
82 |
83 | A dictionary of the show's titles organized by localization.
84 |
85 | ---
86 |
87 | #### descriptions
88 |
89 | A dictionary of the show's descriptions organized by localization.
90 |
91 | ---
92 |
93 | #### start_date
94 |
95 | The show's global release date.
96 |
97 | ---
98 |
99 | #### end_date
100 |
101 | The known show's global end date.
102 |
103 | ---
104 |
105 | #### weekly_airing_day
106 |
107 | The known show's episode release day.
108 |
109 | ```js title="Possible day enum values"
110 | "Sunday": 0,
111 | "Monday": 1,
112 | "Tuesday": 2,
113 | "Wednesday": 3,
114 | "Thursday": 4,
115 | "Friday": 5,
116 | "Saturday": 6
117 | ```
118 |
119 | ---
120 |
121 | #### season_period
122 |
123 | The season on which the show has been released.
124 |
125 | ```js title="Possible season_period enum values"
126 | "WINTER": 0,
127 | "SPRING": 1,
128 | "SUMMER": 2,
129 | "FALL": 3,
130 | "UNKNOWN": 4
131 | ```
132 |
133 | ---
134 |
135 | #### season_year
136 |
137 | The year on which the show has been released.
138 |
139 | ---
140 |
141 | #### episodes_count
142 |
143 | Number of episodes released for the show.
144 |
145 | ---
146 |
147 | #### episode_duration
148 |
149 | The show's episode average duration in minutes.
150 |
151 | ---
152 |
153 | #### trailer_url
154 |
155 | External link to the show's trailer video.
156 | Possible services:
157 |
158 | * Youtube
159 | * Dailymotion
160 |
161 | ---
162 |
163 | #### cover_image
164 |
165 | The show's cover image.
166 |
167 | ---
168 |
169 | #### has_cover_image
170 |
171 | Indicates if the show has an a cover images associated with it.
172 |
173 | ---
174 |
175 | ##### cover_color
176 |
177 | The show's cover main color, in `HEX` format.
178 |
179 | ---
180 |
181 | ##### banner_image
182 |
183 | The show's banner image.
184 |
185 | ---
186 |
187 | #### genres
188 |
189 | A collection of the show's associated genres.
190 | You can find all possible values ***[here](https://api.aniapi.com/v1/resources/1.0/0)***.
191 |
192 | ---
193 |
194 | #### sagas
195 |
196 | A collection of the show's associated sagas.
197 |
198 | ```js title="Saga's object structure"
199 | {
200 | // Map of strings
201 | // Contains the saga's title in various localizations
202 | "titles": {}
203 |
204 | // Map of strings
205 | // Contains the saga's description in various localizations
206 | "descriptions": {}
207 |
208 | // Integer
209 | // The saga's first episode number
210 | "episode_from": 0
211 |
212 | // Integer
213 | // The saga's last episode number
214 | "episode_to": 0
215 |
216 | // Integer
217 | // The saga's total episodes count
218 | "episodes_count": 0
219 | }
220 | ```
221 | ---
222 |
223 | #### sequel
224 |
225 | The show's precedent Anime's unique identifier in story-line.
226 |
227 | ---
228 |
229 | #### prequel
230 |
231 | The show's successive Anime's unique identifier in story-line.
232 |
233 | ---
234 |
235 | #### score
236 |
237 | The show's global appreciation indicator. Minimum value is `0` and maximum is `100`.
238 |
239 | ---
240 |
241 | #### nsfw
242 |
243 | Indicates if the show is marked as ***[NotSafeForWork](https://www.techopedia.com/definition/27847/not-safe-for-work-nsfw)***
244 |
245 | ---
246 |
247 | #### recommendations
248 |
249 | The show's recommended Anime's unique identifier because similar. Ordered by descendent rating.
250 |
251 | ---
252 |
253 | ### Example
254 |
255 | ```js title="Anime object example"
256 | {
257 | "anilist_id": 21,
258 | "mal_id": 21,
259 | "format": 0,
260 | "status": 1,
261 | "titles": {
262 | "rj": "ONE PIECE",
263 | "en": "ONE PIECE",
264 | "jp": "ONE PIECE",
265 | "fr": "One Piece",
266 | "it": "One Piece"
267 | },
268 | "descriptions": {
269 | "en": "Gold Roger was known as the Pirate King, the strongest and mo...",
270 | "fr": "Il fut un temps où Gold Roger était le plus grand de tous les...",
271 | "it": "Monkey D. Rufy è un giovane pirata sognatore che da piccolo h...",
272 | "jp": "海賊王を夢見る少年モンキー・D・ルフィを主人公とする、「ひとつなぎの..."
273 | },
274 | "start_date": "1999-10-20T00:00:00Z",
275 | "end_date": "1970-01-01T00:00:00Z",
276 | "weekly_airing_day": 0,
277 | "season_period": 3,
278 | "season_year": 1999,
279 | "episodes_count": 981,
280 | "episode_duration": 24,
281 | "cover_image": "https://s4.anilist.co/file/anilistcdn/media/anime/cov...",
282 | "cover_color": "#e4a15d",
283 | "banner_image": "https://s4.anilist.co/file/anilistcdn/media/anime/ba...",
284 | "genres": [
285 | "Action",
286 | "Adventure",
287 | "Comedy",
288 | "Drama",
289 | "Fantasy",
290 | "Pirates",
291 | "Shounen",
292 | "Ensemble Cast",
293 | "Super Power",
294 | "Ships",
295 | "Male Protagonist",
296 | "Conspiracy",
297 | "Tragedy",
298 | "Crime",
299 | "Time Skip",
300 | "Politics",
301 | "Boys' Love",
302 | "War",
303 | "Shapeshifting",
304 | "Swordplay",
305 | "Lost Civilization",
306 | "Guns",
307 | "Animals",
308 | "Anachronism",
309 | "Primarily Adult Cast",
310 | "Cyborg",
311 | "Skeleton",
312 | "Espionage",
313 | "Primarily Male Cast",
314 | "Gender Bending",
315 | "Ninja",
316 | "Henshin",
317 | "Real Robot",
318 | "Anti-Hero",
319 | "Mermaid",
320 | "Battle Royale",
321 | "Assassins",
322 | "Tanned Skin",
323 | "Zombie",
324 | "Time Manipulation",
325 | "Kuudere"
326 | ],
327 | "sagas": [{
328 | "titles": {
329 | "en": "East Blue Arc",
330 | "fr": "Saga East Blue",
331 | "it": "Saga del Mare Orientale",
332 | "jp": "1st 東の海編"
333 | },
334 | "descriptions": {
335 | "en": "Influenced by Shanks, Luffy starts his journey to become the ...",
336 | "fr": "Influencé par Shanks, Luffy commence son voyage pour devenir ...",
337 | "it": "La prima stagione si intitola Saga del Mare Orientale (EAST B...",
338 | "jp": "ルフィは、たった1人の海賊として大海原へと旅立つ。海軍基地の..."
339 | },
340 | "episode_from": 1,
341 | "episode_to": 61,
342 | "episodes_count": 61
343 | }, ...
344 | ],
345 | "score": 86,
346 | "recommendations": [
347 | 2022,
348 | 10,
349 | 745,
350 | 2974,
351 | 147,
352 | 416
353 | ],
354 | "nsfw": true,
355 | "has_cover_image": true,
356 | "id": 11
357 | }
358 | ```
359 |
360 | ## Retrieve a specific Anime
361 |
362 |
363 |
364 | Retrieves an Anime show, based on its unique identifier.
365 |
366 | ### Parameters
367 |
368 | No parameters.
369 |
370 | ### Returns
371 |
372 | Returns an Anime object if a valid identifier was provided.
373 |
374 | ### Try it
375 |
376 | export const retrieveAnimeParams = [
377 | { name: ':id', type: 'number', placeholder: ':id', value: '11' }
378 | ];
379 |
380 |
381 |
382 | ## Get a list of Anime
383 |
384 |
385 |
386 | Returns a list of Anime objects.
387 | The Anime are returned sorted by `score`, with the most popular Anime appearing first.
388 |
389 | ### Parameters
390 |
391 | ---
392 |
393 | #### title
394 |
395 | A case-insensitive pattern filter on the list based on the `titles` field values.
396 |
397 | ---
398 |
399 | #### anilist_id
400 |
401 | A filter on the list based on the `anilist_id` field value.
402 |
403 | ---
404 |
405 | #### mal_id
406 |
407 | A filter on the list based on the `mal_id` field value.
408 |
409 | ---
410 |
411 | #### tmdb_id
412 |
413 | A filter on the list based on the `tmdb_id` field value.
414 |
415 | ---
416 |
417 | #### formats
418 |
419 | A filter on the list based on the `format` field value.
420 |
421 | ---
422 |
423 | #### status
424 |
425 | A filter on the list based on the `status` field value.
426 |
427 | ---
428 |
429 | #### year
430 |
431 | A filter on the list based on the `season_year` field value.
432 |
433 | ---
434 |
435 | #### season
436 |
437 | A filter on the list based on the `season_period` field value.
438 |
439 | ---
440 |
441 | #### genres
442 |
443 | A case-sensitive pattern filter on the list based on the `genres` field values.
444 |
445 | ---
446 |
447 | #### nsfw
448 |
449 | A filter on the list which excludes Anime classified as **Not Safe For Work**.
450 |
451 | ---
452 |
453 | #### with_episodes
454 |
455 | A filter on the list which excludes Anime without episodes available.
456 |
457 | ---
458 |
459 | ### Returns
460 |
461 | Returns an array of Anime objects with a size based on the filter provided.
462 |
463 | ### Try it
464 |
465 | export const getListAnimeParams = [
466 | { name: 'title', type: 'text', placeholder: 'title', value: 'One Piece' },
467 | { name: 'anilist_id', type: 'number', placeholder: 'anilist_id', value: '' },
468 | { name: 'mal_id', type: 'number', placeholder: 'mal_id', value: '' },
469 | { name: 'tmdb_id', type: 'number', placeholder: 'tmdb_id', value: '' },
470 | { name: 'formats', type: 'text', placeholder: 'formats', value: '0,1' },
471 | { name: 'status', type: 'text', placeholder: 'status', value: '1' },
472 | { name: 'year', type: 'number', placeholder: 'year', value: '1999' },
473 | { name: 'season', type: 'text', placeholder: 'season', value: '3' },
474 | { name: 'genres', type: 'text', placeholder: 'genres', value: 'Pirates,War,Cyborg' },
475 | { name: 'nsfw', type: 'checkbox', placeholder: 'nsfw', value: true },
476 | { name: 'with_episodes', type: 'checkbox', placeholder: 'with_episodes', value: false },
477 | ];
478 |
479 |
480 |
481 | ## Retrieve random Anime
482 |
483 |
484 |
485 | Retrieves a random Anime show list.
486 |
487 | ### Parameters
488 |
489 | No parameters.
490 |
491 | ### Returns
492 |
493 | Returns a random Anime list.
494 |
495 | ### Try it
496 |
497 | export const retrieveRandomAnimeParams = [
498 | { name: ':count', type: 'number', placeholder: ':count', value: '5' },
499 | { name: ':nsfw', type: 'checkbox', placeholder: 'nsfw', value: true }
500 | ];
501 |
502 |
503 |
--------------------------------------------------------------------------------
/docs/resources/episode.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 2
3 | ---
4 |
5 | # Episode
6 |
7 | import ApiCodeBlock from '../../src/components/ApiCodeBlock';
8 | import Highlight from '../../src/components/Highlight';
9 | import ApiTryIt from '../../src/components/ApiTryIt';
10 |
11 | export const endpoints = [
12 | { method: 'GET', uri: '/v1/episode/:id' },
13 | { method: 'GET', uri: '/v1/episode' },
14 | ];
15 |
16 | This is an object representing an Anime show's Episode.
17 | The API allows you to retrieve individual Episode as well as a list of them using various filters.
18 |
19 |
20 |
21 | ## The Episode object
22 |
23 | ### Attributes
24 |
25 | ---
26 |
27 | #### id
28 |
29 | Unique identifier for an Episode.
30 |
31 | ---
32 |
33 | #### anime_id
34 |
35 | ***[Anime](anime)*** external unique identifier.
36 |
37 | ---
38 |
39 | #### number
40 |
41 | The episode's progressive number referring to the entire show.
42 |
43 | ---
44 |
45 | #### title
46 |
47 | The episode's localized title.
48 |
49 | ---
50 |
51 | #### video
52 |
53 | The episode's original streaming url.
54 |
55 | ---
56 |
57 | #### video_headers
58 |
59 | The `video`'s *HTTP* headers needed to navigate.
60 |
61 | ---
62 |
63 | #### locale
64 |
65 | The episode's website related locale.
66 |
67 | ---
68 |
69 | #### quality
70 |
71 | The episode's streaming quality.
72 |
73 | ---
74 |
75 | #### format
76 |
77 | The episode's streaming codec format.
78 |
79 | ---
80 |
81 | #### is_dub
82 |
83 | Indicates if the episode is dubbed or subbed.
84 |
85 | ---
86 |
87 | ### Example
88 |
89 | ```js title="Episode object example"
90 | {
91 | "anime_id": 11,
92 | "number": 1,
93 | "title": "Il ragazzo di gomma",
94 | "video": "https://api.aniapi.com/v1/proxy/https%3a%2f%2fcdn2.dr...",
95 | "locale": "it",
96 | "quality": 1080,
97 | "format": "mp4",
98 | "is_dub": false,
99 | "id": 485
100 | }
101 | ```
102 |
103 | ## Retrieve a specific Episode
104 |
105 |
106 |
107 | Retrieves an Episode, based on its unique identifier.
108 |
109 | ### Parameters
110 |
111 | No parameters.
112 |
113 | ### Returns
114 |
115 | Returns an Episode object if a valid identifier was provided.
116 |
117 | ### Try it
118 |
119 | export const retrieveEpisodeParams = [
120 | { name: ':id', type: 'number', placeholder: ':id', value: '485' }
121 | ];
122 |
123 |
124 |
125 | ## Get a list of Episode
126 |
127 |
128 |
129 | Returns a list of Episode object.
130 | The Episodes are returned sorted by `anime_id` and `number`.
131 |
132 | ### Parameters
133 |
134 | ---
135 |
136 | #### anime_id
137 |
138 | A filter on the list based on the `anime_id` field value.
139 |
140 | ---
141 |
142 | #### number
143 |
144 | A filter on the list based on the `number` field value.
145 |
146 | ---
147 |
148 | #### is_dub
149 |
150 | A case-sensitive pattern filter on the list based on the `is_dub` field value.
151 |
152 | ---
153 |
154 | #### locale
155 |
156 | A case-sensitive pattern filter on the list based on the `locale` field value.
157 |
158 | ---
159 |
160 | ### Returns
161 |
162 | Returns an array of Episode objects with a size based on the filter provided.
163 |
164 | ### Try it
165 |
166 | export const getListEpisodeParams = [
167 | { name: 'anime_id', type: 'number', placeholder: 'anime_id', value: '11' },
168 | { name: 'number', type: 'number', placeholder: 'number', value: '' },
169 | { name: 'is_dub', type: 'checkbox', placeholder: 'is_dub', value: true },
170 | { name: 'locale', type: 'text', placeholder: 'locale', value: 'it' }
171 | ];
172 |
173 |
--------------------------------------------------------------------------------
/docs/resources/resource.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 4
3 | ---
4 |
5 | # Resource
6 |
7 | import ApiCodeBlock from '../../src/components/ApiCodeBlock';
8 | import Highlight from '../../src/components/Highlight';
9 | import ApiTryIt from '../../src/components/ApiTryIt';
10 |
11 | export const endpoints = [
12 | { method: 'GET', uri: '/v1/resources' },
13 | { method: 'GET', uri: '/v1/resources/:version/:type' },
14 | ];
15 |
16 | AniAPI provides an endpoint to retrieve general purpose content, most of them utils.
17 | The API allows you to retrieve the last available Resources' version and retrieve individual Resource providing a version and a type.
18 |
19 |
20 |
21 | ## Genres
22 |
23 | This is an `array of strings` containing all the availables `genre` value across AniAPI.
24 |
25 | ### Changelog
26 |
27 | | Version | Description |
28 | | --- | --- |
29 | | `1.0` | Default values |
30 |
31 | ## Localizations
32 |
33 | This is an `array of objects` containing all the possible `locale` values inside AniAPI.
34 |
35 | ### Attributes
36 |
37 | ---
38 |
39 | #### i18n
40 |
41 | The locale's **[ISO 639‑1](https://en.wikipedia.org/wiki/ISO_639-1)** language code.
42 |
43 | ---
44 |
45 | #### label
46 |
47 | The locale's description.
48 |
49 | ---
50 |
51 | ### Changelog
52 |
53 | | Version | Description |
54 | | --- | --- |
55 | | `1.0` | Initial support for `en` and `it` values |
56 | | `1.1.5` | Added support for `pl` value |
57 |
58 | ## Retrieve last Resources' version
59 |
60 |
61 |
62 | ### Parameters
63 |
64 | No parameters.
65 |
66 | ### Returns
67 |
68 | Returns a `string` which identifies the latest available Resources' version.
69 |
70 | :::caution
71 |
72 | Older Resources' versions **will be available forever**, in order to serve also external services that don't follow our updates.
73 |
74 | :::
75 |
76 | ### Try it
77 |
78 |
79 |
80 | ## Retrieve a specific Resource
81 |
82 |
83 |
84 | ### Type
85 |
86 | ```js title="Possible type enum values"
87 | "GENRES": 0,
88 | "LOCALIZATIONS": 1
89 | ```
90 |
91 | ---
92 |
93 | ### Returns
94 |
95 | Returns a Resource object according to the `version` and `type` values provided.
96 |
97 | ### Try it
98 |
99 | export const getResourceParams = [
100 | { name: ':version', type: 'text', placeholder: ':version', value: '1.0' },
101 | { name: ':type', type: 'number', placeholder: ':type', value: '0' }
102 | ];
103 |
104 |
105 |
--------------------------------------------------------------------------------
/docs/resources/song.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 3
3 | ---
4 |
5 | # Song
6 |
7 |
8 | import ApiCodeBlock from '../../src/components/ApiCodeBlock';
9 | import Highlight from '../../src/components/Highlight';
10 | import ApiTryIt from '../../src/components/ApiTryIt';
11 |
12 | export const endpoints = [
13 | { method: 'GET', uri: '/v1/song/:id' },
14 | { method: 'GET', uri: '/v1/song' },
15 | { method: 'GET', uri: '/v1/random/song/:count' },
16 | ];
17 |
18 | This is an object representing an Anime show's opening or ending song.
19 | The API allows you to retrieve individual Song as well as a list of them using various filters.
20 |
21 |
22 |
23 | ## The Song object
24 |
25 | ### Attributes
26 |
27 | ---
28 |
29 | #### id
30 |
31 | Unique identifier for a Song.
32 |
33 | ---
34 |
35 | #### anime_id
36 |
37 | ***[Anime](anime)*** external unique identifier.
38 |
39 | ---
40 |
41 | #### title
42 |
43 | The song's original title.
44 |
45 | ---
46 |
47 | #### artist
48 |
49 | The song's artist name.
50 |
51 | ---
52 |
53 | #### album
54 |
55 | The song's album name.
56 |
57 | ---
58 |
59 | #### year
60 |
61 | The song's release year.
62 |
63 | ---
64 |
65 | #### season
66 |
67 | The song's release season.
68 |
69 | ```js title="Possible season enum values"
70 | "WINTER": 0,
71 | "SPRING": 1,
72 | "SUMMER": 2,
73 | "FALL": 3,
74 | "UNKNOWN": 4
75 | ```
76 |
77 | ---
78 |
79 | #### duration
80 |
81 | The song's duration in milliseconds.
82 |
83 | ---
84 |
85 | #### preview_url
86 |
87 | The song's Spotify preview url, which provides ~`30` seconds of the song.
88 |
89 | ---
90 |
91 | #### open_spotify_url
92 |
93 | The song's ***[Spotify Web Player](https://open.spotify.com/)*** url to listen to it.
94 |
95 | ---
96 |
97 | #### local_spotify_url
98 |
99 | The song's ***[Spotify App](https://www.spotify.com/it/download/)*** url to listen to it.
100 | This url will open your local Spotify application automatically.
101 |
102 | ---
103 |
104 | #### type
105 |
106 | The song's type.
107 |
108 | ```js title="Possible type enum values"
109 | "OPENING": 0,
110 | "ENDING": 1,
111 | "NONE": 2
112 | ```
113 |
114 | ---
115 |
116 | ### Example
117 |
118 | ```js title="Song object example"
119 | {
120 | "anime_id": 1,
121 | "title": "The Real Folk Blues",
122 | "artist": "Mai Yamane",
123 | "album": "COWBOY BEBOP Vitaminless",
124 | "year": 1998,
125 | "season": 1,
126 | "duration": 377066,
127 | "preview_url": "https://p.scdn.co/mp3-preview/a2226076e4b89e16d8827189fd32da4535d369b4?cid=b074c52808fb4394a28785f381872ea2",
128 | "open_spotify_url": "https://open.spotify.com/track/5Cmf3LmbLd9g79rS55X7qK",
129 | "local_spotify_url": "spotify:track:5Cmf3LmbLd9g79rS55X7qK",
130 | "type": 1,
131 | "id": 1
132 | }
133 | ```
134 |
135 | ## Retrieve a specific Song
136 |
137 |
138 |
139 | Retrieves an Anime show's Song, based on its unique identifier.
140 |
141 | ### Parameters
142 |
143 | No parameters.
144 |
145 | ### Returns
146 |
147 | Returns a Song object if a valid identifier was provided.
148 |
149 | ### Try it
150 |
151 | export const retrieveSongParams = [
152 | { name: ':id', type: 'number', placeholder: ':id', value: '11' }
153 | ];
154 |
155 |
156 |
157 |
158 | ## Get a list of Song
159 |
160 |
161 |
162 | Returns a list of Song objects.
163 | The Songs are returned sorted by `year` descending, after by `season` descending.
164 |
165 | ### Parameters
166 |
167 | ---
168 |
169 | #### anime_id
170 |
171 | A filter on the list based on the `anime_id` field value.
172 |
173 | ---
174 |
175 | #### title
176 |
177 | A case-insensitive pattern filter on the list based on the `title` field value.
178 |
179 | ---
180 |
181 | #### artist
182 |
183 | A case-insensitive pattern filter on the list based on the `artist` field value.
184 |
185 | ---
186 |
187 | #### year
188 |
189 | A filter on the list based on the `year` field value.
190 |
191 | ---
192 |
193 | #### season
194 |
195 | A filter on the list based on the `season` field value.
196 |
197 | ---
198 |
199 | #### type
200 |
201 | A filter on the list based on the `type` field value.
202 |
203 | ---
204 |
205 | ### Returns
206 |
207 | Returns an array of Anime objects with a size based on the filter provided.
208 |
209 | ### Try it
210 |
211 | export const getListSongParams = [
212 | { name: 'anime_id', type: 'number', placeholder: 'anime_id', value: '10' },
213 | { name: 'title', type: 'text', placeholder: 'title', value: '' },
214 | { name: 'artist', type: 'text', placeholder: 'artist', value: 'FLOW' },
215 | { name: 'year', type: 'text', placeholder: 'year', value: '' },
216 | { name: 'season', type: 'text', placeholder: 'season', value: '' },
217 | { name: 'type', type: 'text', placeholder: 'type', value: '' },
218 | ];
219 |
220 |
221 |
222 | ## Retrieve random Songs
223 |
224 |
225 |
226 | Retrieves a random Anime show's Song list.
227 |
228 | ### Parameters
229 |
230 | No parameters.
231 |
232 | ### Returns
233 |
234 | Returns a random Song list.
235 |
236 | ### Try it
237 |
238 | export const retrieveRandomSongParams = [
239 | { name: ':count', type: 'number', placeholder: ':count', value: '5' }
240 | ];
241 |
242 |
--------------------------------------------------------------------------------
/docs/resources/user.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 5
3 | ---
4 |
5 | # User
6 |
7 | import ApiCodeBlock from '../../src/components/ApiCodeBlock';
8 | import Highlight from '../../src/components/Highlight';
9 | import ApiTryIt from '../../src/components/ApiTryIt';
10 |
11 | export const endpoints = [
12 | { method: 'GET', uri: '/v1/user/:id' },
13 | { method: 'GET', uri: '/v1/user' },
14 | { method: 'POST', uri: '/v1/user' },
15 | { method: 'DELETE', uri: '/v1/user' }
16 | ];
17 |
18 | This is an object representing an User.
19 | The API allows you to retrieve individual User as well as a list of them using various filters.
20 | Furthermore it lets you update and delete an User.
21 |
22 |
23 |
24 | ## The User object
25 |
26 | ### Attributes
27 |
28 | ---
29 |
30 | #### id
31 |
32 | Unique identifier for an User.
33 |
34 | ---
35 |
36 | #### username
37 |
38 | The User's username.
39 |
40 | ---
41 |
42 | #### email
43 |
44 | The User's email.
45 |
46 | :::note
47 |
48 | This field is obscured[^1] to not-authorized in order to avoid email adresses' collecting behaviours.
49 |
50 | :::
51 |
52 | ---
53 |
54 | #### email_verified
55 |
56 | Indicates if the User has already confirmed their email.
57 |
58 | :::note
59 |
60 | This field is obscured[^1] to not-authorized.
61 |
62 | :::
63 |
64 | ---
65 |
66 | #### role
67 |
68 | The User's role inside AniAPI.
69 |
70 | ```js title="Possible role enum values"
71 | "BASIC": 0,
72 | "MODERATOR": 1,
73 | "ADMINISTRATOR": 2
74 | ```
75 |
76 | ---
77 |
78 | #### avatar
79 |
80 | The User's avatar.
81 | This value is imported from external User's trackers.
82 |
83 | ---
84 |
85 | #### gender
86 |
87 | The User's gender.
88 |
89 | ```js title="Possible gender enum values"
90 | "UNKNOWN": 0,
91 | "MALE": 1,
92 | "FEMALE": 2
93 | ```
94 |
95 | ---
96 |
97 | #### localization
98 |
99 | The User's preferred locale reference.
100 |
101 | :::info
102 |
103 | Check on **[Resource](Resource#changelog-1)** for further details on locales.
104 |
105 | :::
106 |
107 | ---
108 |
109 | #### has_anilist
110 |
111 | Indicates if the User has linked their ***[AniList](https://anilist.co/)*** account with AniAPI.
112 |
113 | ---
114 |
115 | #### has_mal
116 |
117 | Indicates if the User has linked their ***[MyAnimeList](https://myanimelist.net/)*** account with AniAPI.
118 |
119 | ---
120 |
121 | ### Example
122 |
123 | ```js title="User object example"
124 | {
125 | "username": "Dazorn",
126 | "role": 0,
127 | "gender": 1,
128 | "avatar": "https://s4.anilist.co/file/anilistcdn/user/avatar/large/b192651-setw7IgPvmZS.jpg",
129 | "id": 1
130 | }
131 | ```
132 |
133 | ## Retrieve a specific User
134 |
135 |
136 |
137 | Retrieves an User, based on its unique identifier.
138 |
139 | ### Parameters
140 |
141 | No parameters.
142 |
143 | ### Returns
144 |
145 | Returns an obscured[^1] User object if a valid identifier was provided.
146 |
147 | ### Try it
148 |
149 | export const retrieveUserParams = [
150 | { name: ':id', type: 'number', placeholder: ':id', value: '1' }
151 | ];
152 |
153 |
154 |
155 | ## Get a list of User
156 |
157 |
158 |
159 | Returns a list of obscured[^1] User objects.
160 | The Users are returned sorted by `username`, following alphabetical ascending order.
161 |
162 | ### Parameters
163 |
164 | ---
165 |
166 | #### username
167 |
168 | A case-insensitive pattern filter on the list based on the `username` field value.
169 |
170 | ---
171 |
172 | #### email
173 |
174 | A case-sensitive filter on the list based on the `email` field value.
175 |
176 | ---
177 |
178 | ### Returns
179 |
180 | Returns an array of obscured[^1] User objects with a size based on the filter provided.
181 |
182 | ### Try it
183 |
184 | export const getListUserParams = [
185 | { name: 'username', type: 'text', placeholder: 'username', value: 'Daz' },
186 | { name: 'email', type: 'text', placeholder: 'email', value: '' }
187 | ];
188 |
189 |
190 |
191 | ## Update an User
192 |
193 | :::warning
194 |
195 | We recommend you to not implement an User's update data form.
196 |
197 | Instead, we strongly suggest you to redirect the User to the **[profile](../../profile)** web page and to let us do the rest ♥.
198 |
199 | :::
200 |
201 |
202 |
203 | Updates an User based on the provided values.
204 |
205 | ### Parameters
206 |
207 | ---
208 |
209 | #### id
210 |
211 | The User's id to update
212 |
213 | ---
214 |
215 | #### password
216 |
217 | The User's new password value.
218 |
219 | ---
220 |
221 | #### gender
222 |
223 | The User's gender value.
224 |
225 | ---
226 |
227 | #### localization
228 |
229 | The User's new localization value.
230 |
231 | ---
232 |
233 | #### anilist_id
234 |
235 | The User's ***[AniList](https://anilist.co/)*** account external id.
236 |
237 | ---
238 |
239 | #### anilist_token
240 |
241 | The User's ***[AniList](https://anilist.co/)*** account external token.
242 | This value becomes `required` when you provide the `anilist_id` field.
243 |
244 | ---
245 |
246 | ### Returns
247 |
248 | Returns the updated User object.
249 |
250 | ### Example
251 |
252 | ```js title="Example User update request"
253 | fetch('https://api.aniapi.com/v1/user', {
254 | method: 'POST',
255 | headers: {
256 | 'Authorization': 'Bearer ',
257 | 'Content-Type': 'application/json',
258 | 'Accept': 'application/json'
259 | },
260 | body: {
261 | id: 1,
262 | gender: 1,
263 | localization: 'it'
264 | }
265 | });
266 | ```
267 |
268 | ```js title="Example User update response"
269 | {
270 | "status_code": 200,
271 | "message": "User updated",
272 | "data": {
273 | "username": "Dazorn",
274 | "role": 0,
275 | "gender": 1,
276 | "localization": "it",
277 | "has_anilist": true,
278 | "has_mal": false,
279 | "id": 1
280 | },
281 | "version": "1"
282 | }
283 | ```
284 |
285 | ## Delete an User
286 |
287 |
288 |
289 | Deletes an User based on the provided unique identifier.
290 |
291 | ### Parameters
292 |
293 | No parameters.
294 |
295 | ### Returns
296 |
297 | No particular return.
298 |
299 | ### Example
300 |
301 | ```js title="Example User delete request"
302 | fetch('https://api.aniapi.com/v1/user/1', {
303 | method: 'DELETE',
304 | headers: {
305 | 'Authorization': 'Bearer ',
306 | 'Content-Type': 'application/json',
307 | 'Accept': 'application/json'
308 | }
309 | });
310 | ```
311 |
312 | ```js title="Example User delete response"
313 | {
314 | "status_code": 200,
315 | "message": "User deleted",
316 | "data": "",
317 | "version": "1"
318 | }
319 | ```
320 |
321 | [^1]: An obscured object has certain fields hidden
322 |
--------------------------------------------------------------------------------
/docs/resources/user_story.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 6
3 | ---
4 |
5 | # UserStory
6 |
7 | import ApiCodeBlock from '../../src/components/ApiCodeBlock';
8 | import Highlight from '../../src/components/Highlight';
9 | import ApiTryIt from '../../src/components/ApiTryIt';
10 |
11 | export const endpoints = [
12 | { method: 'GET', uri: '/v1/user_story/:id' },
13 | { method: 'GET', uri: '/v1/user_story' },
14 | { method: 'PUT', uri: '/v1/user_story' },
15 | { method: 'POST', uri: '/v1/user_story' },
16 | { method: 'DELETE', uri: '/v1/user_story' }
17 | ];
18 |
19 | This is an object representing an User's watch history.
20 | The API allows you to retrieve individual UserStory as well as a list of them using various filters.
21 | Furthermore it lets you create, update and delete an UserStory.
22 |
23 | UserStory is used to synchronize data between AniAPI and external Anime tracking systems.
24 |
25 |
26 |
27 | ## The UserStory object
28 |
29 | ### Attributes
30 |
31 | ---
32 |
33 | #### id
34 |
35 | Unique identifier for an UserStory.
36 |
37 | ---
38 |
39 | #### user_id
40 |
41 | ***[User](user)*** external unique identifier.
42 |
43 | ---
44 |
45 | #### anime_id
46 |
47 | ***[Anime](anime)*** external unique identifier.
48 |
49 | ---
50 |
51 | #### status
52 |
53 | The UserStory's watching status.
54 |
55 | ```js title="Possible status enum values"
56 | "CURRENT": 0,
57 | "PLANNING": 1,
58 | "COMPLETED": 2,
59 | "DROPPED": 3,
60 | "PAUSED": 4,
61 | "REPEATING": 5
62 | ```
63 |
64 | ---
65 |
66 | #### current_episode
67 |
68 | The UserStory's watching progress.
69 |
70 | ---
71 |
72 | #### current_episode_ticks
73 |
74 | The UserStory's `current_episode` watching time in milliseconds.
75 |
76 | ---
77 |
78 | ### Example
79 |
80 | ```js title="UserStory object example"
81 | {
82 | "user_id": 1,
83 | "anime_id": 10,
84 | "status": 2,
85 | "current_episode": 220,
86 | "current_episode_ticks": 0,
87 | "id": 27
88 | }
89 | ```
90 |
91 | ## Retrieve a specific UserStory
92 |
93 |
94 |
95 | Retrieves an UserStory, based on its unique identifier.
96 |
97 | ### Parameters
98 |
99 | No parameters.
100 |
101 | ### Returns
102 |
103 | Returns an UserStory object if a valid identifier was provided and the authenticated User owns the rights to get it.
104 |
105 | ### Try it
106 |
107 | export const retrieveUserStoryParams = [
108 | { name: ':id', type: 'number', placeholder: ':id', value: '1' }
109 | ];
110 |
111 |
112 |
113 | ## Get a list of UserStory
114 |
115 |
116 |
117 | Returns a list of UserStory objects.
118 | The UserStories are returned sorted by `creation_date`, following descending order.
119 |
120 | :::info
121 |
122 | As default, it will return all the UserStories owned by the request's authenticated User using the `user_id` filter's field.
123 |
124 | :::
125 |
126 | ### Parameters
127 |
128 | ---
129 |
130 | #### anime_id
131 |
132 | A filter on the list based on the `anime_id` field value.
133 |
134 | ---
135 |
136 | #### user_id
137 |
138 | A filter on the list based on the `user_id` field value.
139 |
140 | ---
141 |
142 | #### status
143 |
144 | A filter on the list based on the `status` field value.
145 |
146 | ---
147 |
148 | #### synced
149 |
150 | A filter on the list based on the `synced` field value.
151 | `synced` field indicates if an UserStory has been synchronized with User's linked trackers.
152 |
153 | ---
154 |
155 | ### Returns
156 |
157 | Returns an array of UserStory objects with a size based on the filter provided.
158 |
159 | ### Try it
160 |
161 | export const getListUserStoryParams = [
162 | { name: 'anime_id', type: 'number', placeholder: 'anime_id', value: '' },
163 | { name: 'user_id', type: 'number', placeholder: 'user_id', value: '' },
164 | { name: 'status', type: 'number', placeholder: 'status', value: '' },
165 | { name: 'synced', type: 'checkbox', placeholder: 'synced', value: true },
166 | ];
167 |
168 |
169 |
170 | ## Create an UserStory
171 |
172 |
173 |
174 | Creates an UserStory based on the provided values.
175 |
176 | ### Parameters
177 |
178 | ---
179 |
180 | #### user_id
181 |
182 | The ***[User](user)***'s id that own the UserStory.
183 |
184 | ---
185 |
186 | #### anime_id
187 |
188 | The UserStory's ***[Anime](anime)***.
189 |
190 | ---
191 |
192 | #### status
193 |
194 | The UserStory's watching status.
195 |
196 | ---
197 |
198 | #### current_episode
199 |
200 | The UserStory's watching progress.
201 | Must be equal or less than the ***[Anime](anime)***'s `episodes_count` value.
202 | When you provide a `status` equal to `1` or `2` this field is auto-calculated.
203 |
204 | ---
205 |
206 | #### current_episode_ticks
207 |
208 | The UserStory's `current_episode` watching time in milliseconds.
209 |
210 | ---
211 |
212 | ### Returns
213 |
214 | Returns the created UserStory object.
215 |
216 | ### Example
217 |
218 | ```js title="Example UserStory create request"
219 | fetch('https://api.aniapi.com/v1/user_story', {
220 | method: 'PUT',
221 | headers: {
222 | 'Authorization': 'Bearer ',
223 | 'Content-Type': 'application/json',
224 | 'Accept': 'application/json'
225 | },
226 | body: {
227 | user_id: 1,
228 | anime_id: 10,
229 | status: 2
230 | }
231 | });
232 | ```
233 |
234 | ```js title="Example UserStory create response"
235 | {
236 | "status_code": 200,
237 | "message": "Story created",
238 | "data": {
239 | "user_id": 1,
240 | "anime_id": 10,
241 | "status": 2,
242 | "current_episode": 220,
243 | "current_episode_ticks": 0,
244 | "id": 1
245 | },
246 | "version": "1"
247 | }
248 | ```
249 |
250 | ## Update an UserStory
251 |
252 |
253 |
254 | Updates an UserStory based on the provided values.
255 |
256 | ### Parameters
257 |
258 | ---
259 |
260 | #### id
261 |
262 | The UserStory's unique identifier.
263 |
264 | ---
265 |
266 | #### user_id
267 |
268 | The ***[User](user)***'s id that owns the UserStory.
269 |
270 | ---
271 |
272 | #### anime_id
273 |
274 | The UserStory's ***[Anime](anime)***.
275 |
276 | ---
277 |
278 | #### status
279 |
280 | The UserStory's watching status.
281 |
282 | ---
283 |
284 | #### current_episode
285 |
286 | The UserStory's watching progress.
287 | Must be equal or less than the ***[Anime](anime)***'s `episodes_count` value.
288 |
289 | ---
290 |
291 | #### current_episode_ticks
292 |
293 | The UserStory's `current_episode` watching time in milliseconds.
294 |
295 | ---
296 |
297 | ### Returns
298 |
299 | Returns the updated UserStory object.
300 |
301 | ### Example
302 |
303 | ```js title="Example UserStory update request"
304 | fetch('https://api.aniapi.com/v1/user_story', {
305 | method: 'POST',
306 | headers: {
307 | 'Authorization': 'Bearer ',
308 | 'Content-Type': 'application/json',
309 | 'Accept': 'application/json'
310 | },
311 | body: {
312 | id: 27,
313 | user_id: 1,
314 | anime_id: 10,
315 | status: 0,
316 | current_episode: 140,
317 | current_episode_ticks: 1200000
318 | }
319 | });
320 | ```
321 |
322 | ```js title="Example UserStory update response"
323 | {
324 | "status_code": 200,
325 | "message": "Story updated",
326 | "data": {
327 | "user_id": 1,
328 | "anime_id": 10,
329 | "status": 0,
330 | "current_episode": 140,
331 | "current_episode_ticks": 1200000,
332 | "id": 27
333 | },
334 | "version": "1"
335 | }
336 | ```
337 |
338 | ## Delete an UserStory
339 |
340 |
341 |
342 | Deletes an UserStory based on the provided unique identifier.
343 |
344 | :::caution
345 |
346 | You should use this endpoint only when the User has zero linked trackers.
347 | Otherwise the UserStory will get re-imported!
348 |
349 | :::
350 |
351 | ### Parameters
352 |
353 | No parameters.
354 |
355 | ### Returns
356 |
357 | No particular return.
358 |
359 | ### Example
360 |
361 | ```js title="Example UserStory delete request"
362 | fetch('https://api.aniapi.com/v1/user_story/27', {
363 | method: 'DELETE',
364 | headers: {
365 | 'Authorization': 'Bearer ',
366 | 'Content-Type': 'application/json',
367 | 'Accept': 'application/json'
368 | }
369 | });
370 | ```
371 |
372 | ```js title="Example UserStory delete response"
373 | {
374 | "status_code": 200,
375 | "message": "Story deleted",
376 | "data": "",
377 | "version": "1"
378 | }
379 | ```
380 |
--------------------------------------------------------------------------------
/docs/versioning.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar_position: 7
3 | ---
4 |
5 | # Versioning
6 |
7 | As AniAPI could change in future times, we use a versioning system to upgrade and mantain it.
8 | Each API request must provide the wanted API version inside the **[URL](https://en.wikipedia.org/wiki/URL)**:
9 |
10 | ```http title="Example request for v1 AniAPI"
11 | https://api.aniapi.com/v1/anime
12 | ```
13 |
14 | Latest (and current) version available is **v1** API.
15 |
16 | Stay in touch with our **[Patreon](https://patreon.com/aniapi)** to know when a newer version will be released.
17 |
--------------------------------------------------------------------------------
/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | const lightCodeTheme = require('prism-react-renderer/themes/github');
2 | const darkCodeTheme = require('prism-react-renderer/themes/dracula');
3 |
4 | /** @type {import('@docusaurus/types').DocusaurusConfig} */
5 | module.exports = {
6 | title: 'AniAPI',
7 | tagline: 'Your favourite anime WebAPI ♥',
8 | url: 'https://aniapi.com',
9 | baseUrl: '/',
10 | onBrokenLinks: 'throw',
11 | onBrokenMarkdownLinks: 'warn',
12 | favicon: 'img/favicon.ico',
13 | organizationName: 'AniAPI-Team',
14 | projectName: 'AniAPI-Docs',
15 | themeConfig: {
16 | /*algolia: {
17 | apiKey: '',
18 | indexName: ''
19 | },*/
20 | gtag: {
21 | trackingID: 'G-JBHTBTX1HR',
22 | anonymizeIP: true
23 | },
24 | colorMode: {
25 | defaultMode: 'dark',
26 | disableSwitch: true
27 | },
28 | navbar: {
29 | title: 'AniAPI Docs',
30 | logo: {
31 | alt: 'My Site Logo',
32 | src: 'img/aniapi_icon.png',
33 | },
34 | items: [
35 | {
36 | type: 'doc',
37 | docId: 'intro',
38 | position: 'left',
39 | label: 'Docs'
40 | },
41 | {
42 | to: 'login',
43 | label: 'Login',
44 | position: 'right'
45 | },
46 | {
47 | to: 'https://patreon.com/aniapi',
48 | prependBaseUrlToHref: false,
49 | className: 'icon fab fa-patreon',
50 | title: 'Patreon',
51 | position: 'right'
52 | },
53 | {
54 | to: 'https://github.com/AniAPI-Team/AniAPI',
55 | prependBaseUrlToHref: false,
56 | className: 'icon fab fa-github',
57 | title: 'Github',
58 | position: 'right'
59 | },
60 | {
61 | to: 'https://discord.gg/xQjZx5aWkR',
62 | prependBaseUrlToHref: false,
63 | className: 'icon fab fa-discord',
64 | title: 'Join discord',
65 | position: 'right'
66 | }
67 | ],
68 | },
69 | footer: {
70 | style: 'dark',
71 | links: [
72 | {
73 | title: 'Docs',
74 | items: [
75 | {
76 | label: 'Introduction',
77 | to: '/docs/',
78 | },
79 | {
80 | label: 'OAuth',
81 | to: '/docs/oauth/implicit_grant'
82 | },
83 | {
84 | label: 'Resources',
85 | to: '/docs/resources/anime'
86 | }
87 | ],
88 | },
89 | {
90 | title: 'Community',
91 | items: [
92 | {
93 | label: 'Github',
94 | href: 'https://github.com/AniAPI-Team',
95 | },
96 | {
97 | label: 'Discord',
98 | href: 'https://discord.gg/xQjZx5aWkR',
99 | }
100 | ],
101 | },
102 | {
103 | title: 'More',
104 | items: [
105 | {
106 | label: 'Patreon',
107 | to: 'https://patreon.com/aniapi'
108 | },
109 | {
110 | label: 'Status',
111 | to: 'https://status.aniapi.com'
112 | }
113 | ],
114 | },
115 | ],
116 | copyright: `Copyright © ${new Date().getFullYear()} AniAPI, Inc. Built with Docusaurus.`,
117 | },
118 | prism: {
119 | theme: lightCodeTheme,
120 | darkTheme: darkCodeTheme,
121 | },
122 | },
123 | presets: [
124 | [
125 | '@docusaurus/preset-classic',
126 | {
127 | docs: {
128 | sidebarPath: require.resolve('./sidebars.js'),
129 | // Please change this to your repo.
130 | editUrl:
131 | 'https://github.com/AniAPI-Team/AniAPI-Docs/edit/main/',
132 | },
133 | theme: {
134 | customCss: require.resolve('./src/css/custom.css'),
135 | }
136 | },
137 | ],
138 | ],
139 | plugins: [
140 | [
141 | 'docusaurus2-dotenv',
142 | {
143 | path: './.env',
144 | systemvars: true,
145 | silent: true
146 | }
147 | ]
148 | ],
149 | themes: [
150 | '@docusaurus/theme-live-codeblock'
151 | ],
152 | scripts: [
153 | 'https://kit.fontawesome.com/4c4bb12a1a.js',
154 | 'https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js'
155 | ]
156 | };
157 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aniapi-docs",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "docusaurus": "docusaurus",
7 | "start": "docusaurus start",
8 | "build": "docusaurus build",
9 | "swizzle": "docusaurus swizzle",
10 | "deploy": "docusaurus deploy",
11 | "clear": "docusaurus clear",
12 | "serve": "docusaurus serve",
13 | "write-translations": "docusaurus write-translations",
14 | "write-heading-ids": "docusaurus write-heading-ids"
15 | },
16 | "dependencies": {
17 | "@docusaurus/core": "2.0.0-beta.2",
18 | "@docusaurus/preset-classic": "2.0.0-beta.2",
19 | "@docusaurus/theme-live-codeblock": "2.0.0-beta.2",
20 | "@docusaurus/theme-search-algolia": "2.0.0-beta.2",
21 | "@mdx-js/react": "^1.6.21",
22 | "@svgr/webpack": "^5.5.0",
23 | "clsx": "^1.1.1",
24 | "crypto": "^1.0.1",
25 | "crypto-browserify": "^3.12.0",
26 | "crypto-js": "^4.1.1",
27 | "disqus-react": "^1.1.1",
28 | "docusaurus2-dotenv": "^1.4.0",
29 | "file-loader": "^6.2.0",
30 | "pkce-challenge": "^2.2.0",
31 | "prism-react-renderer": "^1.2.1",
32 | "react": "^17.0.1",
33 | "react-dom": "^17.0.1",
34 | "url-loader": "^4.1.1"
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.5%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | }
48 | }
--------------------------------------------------------------------------------
/sidebars.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creating a sidebar enables you to:
3 | - create an ordered group of docs
4 | - render a sidebar for each doc of that group
5 | - provide next/previous navigation
6 |
7 | The sidebars can be generated from the filesystem, or explicitly defined here.
8 |
9 | Create as many sidebars as you want.
10 | */
11 |
12 | module.exports = {
13 | // By default, Docusaurus generates a sidebar from the docs folder structure
14 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
15 |
16 | // But you can create a sidebar manually
17 | /*
18 | tutorialSidebar: [
19 | {
20 | type: 'category',
21 | label: 'Tutorial',
22 | items: ['hello'],
23 | },
24 | ],
25 | */
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/ApiCodeBlock.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './ApiCodeBlock.module.css';
3 |
4 | export default function ApiCodeBlock(params) {
5 | const items = [];
6 |
7 | const getStyleByMethod = (method) => {
8 | switch (method) {
9 | case 'GET': return styles.getMethod;
10 | case 'PUT': return styles.putMethod;
11 | case 'POST': return styles.postMethod;
12 | case 'DELETE': return styles.deleteMethod;
13 | }
14 | }
15 |
16 | for (let i = 0; i < params.items.length; i++) {
17 | const method = params.items[i].method;
18 | const uri = params.items[i].uri;
19 |
20 | items.push((
21 |
22 |
23 | {method}
24 |
25 | {uri}
26 |
27 | ));
28 | }
29 |
30 | return (
31 |
32 | {params.title && (
33 |
34 | {params.title}
35 |
36 | )}
37 |
38 | {items}
39 |
40 |
41 | );
42 | }
--------------------------------------------------------------------------------
/src/components/ApiCodeBlock.module.css:
--------------------------------------------------------------------------------
1 | .block {
2 | margin-bottom: var(--ifm-leading);
3 | background-color: rgb(40, 42, 54);
4 | color: rgb(248, 248, 242);
5 | border-radius: var(--ifm-global-radius);
6 | font-size: var(--ifm-code-font-size);
7 | }
8 |
9 | .blockHead {
10 | padding: .75rem var(--ifm-pre-padding);
11 | border-bottom: 1px solid var(--ifm-color-emphasis-300);
12 | font-weight: 500;
13 | }
14 |
15 | .blockBody {
16 | padding: var(--ifm-pre-padding);
17 | line-height: 2rem;
18 | letter-spacing: 1px;
19 | font-size: 90%;
20 | }
21 |
22 | .method {
23 | display: inline-block;
24 | min-width: 80px;
25 | max-width: 80px;
26 | width: 80px;
27 | margin-left: 1rem;
28 | font-weight: 700;
29 | }
30 |
31 | .getMethod {
32 | color: #196bb2;
33 | }
34 |
35 | .putMethod {
36 | color: #c48536;
37 | }
38 |
39 | .postMethod {
40 | color: #1fa44e;
41 | }
42 |
43 | .deleteMethod {
44 | color: #a22027;
45 | }
46 |
47 | .param {
48 |
49 | }
--------------------------------------------------------------------------------
/src/components/ApiTryIt.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { useState } from 'react';
3 | import CodeBlock from '@theme/CodeBlock';
4 | import styles from './ApiTryIt.module.css';
5 |
6 | export default function ApiTryIt(props) {
7 | const items = [];
8 | const values = [];
9 | const uri = `https://api.aniapi.com${props.uri}`;
10 | const method = props.method;
11 | const secure = props.secure ? props.secure : false;
12 |
13 | const [changed, setChanged] = useState(true);
14 | const [mounted, setMounted] = useState(false);
15 | const [timer, setTimer] = useState(null);
16 | const [fetching, setFetching] = useState(false);
17 |
18 | const [url, setUrl] = useState('');
19 | const [json, setJson] = useState('');
20 |
21 | useEffect(() => {
22 | if (mounted) {
23 | onValueChanged();
24 | }
25 | }, [values])
26 |
27 | useEffect(() => {
28 | setMounted(true);
29 | }, []);
30 |
31 | useEffect(() => {
32 | return () => {
33 | setMounted(false);
34 | }
35 | }, []);
36 |
37 | const onValueChanged = async () => {
38 | if (!changed) {
39 | return;
40 | }
41 |
42 | setChanged(false);
43 |
44 | let _uri = uri;
45 | let _init = false;
46 |
47 | for (let i = 0; i < values.length; i++) {
48 | const value = values[i];
49 |
50 | if (typeof (value.value) !== 'boolean' && !value.value) {
51 | continue;
52 | }
53 |
54 | if (value.name.indexOf(':') !== -1) {
55 | _uri = _uri.replace(value.name, value.value);
56 | }
57 | else {
58 | if (!_init) {
59 | _uri += '?';
60 | _init = true;
61 | }
62 | else {
63 | _uri += '&';
64 | }
65 |
66 | if (typeof (value.value) === 'string' && value.value.indexOf(',') !== -1) {
67 | const _values = value.value.split(',');
68 |
69 | _uri += `${value.name}=`;
70 |
71 | for (let j = 0; j < _values.length; j++) {
72 | _uri += `${encodeURIComponent(_values[j])}`;
73 |
74 | if ((j + 1) < _values.length) {
75 | _uri += ',';
76 | }
77 | }
78 | }
79 | else {
80 | _uri += `${value.name}=${encodeURIComponent(value.value)}`;
81 | }
82 | }
83 | }
84 |
85 | const user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
86 |
87 | if (!user && secure) {
88 | setJson('// You need to login in order to perform this request!');
89 | return;
90 | }
91 |
92 | let _json = '';
93 |
94 | try {
95 | setFetching(true);
96 |
97 | let headers = {
98 | 'Accept': 'application/json',
99 | 'Content-Type': 'application/json'
100 | };
101 |
102 | if (secure) {
103 | headers['Authorization'] = `Bearer ${user.access_token}`;
104 | }
105 |
106 | const response = await fetch(_uri, {
107 | method: method,
108 | headers: headers
109 | });
110 | _json = await response.json();
111 | }
112 | catch (ex) {
113 | setFetching(false);
114 | setJson('// Something bad happened, check console (F12)');
115 | return;
116 | }
117 |
118 | setUrl(_uri);
119 | setJson(JSON.stringify(_json, null, 4));
120 |
121 | setFetching(false);
122 | }
123 |
124 | if (props.items) {
125 | for (let i = 0; i < props.items.length; i++) {
126 | const item = props.items[i];
127 |
128 | const [value, setValue] = useState({
129 | name: item.name,
130 | value: item.value
131 | });
132 |
133 | values.push(value);
134 |
135 | switch (item.type) {
136 | case 'number':
137 | case 'text':
138 | items.push((
139 |
140 |
141 | {
146 | setValue(prevState => ({
147 | ...prevState,
148 | value: e.target.value
149 | }));
150 |
151 | if (timer) {
152 | clearTimeout(timer);
153 | setTimer(null);
154 | }
155 |
156 | setTimer(
157 | setTimeout(() => {
158 | setChanged(true);
159 | }, 500)
160 | );
161 | }} />
162 |
163 | ));
164 | break;
165 | case 'checkbox':
166 | items.push((
167 |
168 |
169 | {
174 | setValue(prevState => ({
175 | ...prevState,
176 | value: e.target.checked
177 | }));
178 |
179 | if (timer) {
180 | clearTimeout(timer);
181 | setTimer(null);
182 | }
183 |
184 | setTimer(
185 | setTimeout(() => {
186 | setChanged(true);
187 | }, 500)
188 | );
189 | }} />
190 |
191 | ));
192 | break;
193 | }
194 | }
195 | }
196 |
197 | return (
198 |
199 |
200 | This feature is not available on mobile.
201 |
202 |
203 |
204 | {items}
205 |
206 |
207 | {fetching && (
208 |
209 | )}
210 | {json}
211 |
212 |
213 |
214 | );
215 | }
--------------------------------------------------------------------------------
/src/components/ApiTryIt.module.css:
--------------------------------------------------------------------------------
1 | .tryIt {
2 |
3 | }
4 |
5 | .items {
6 | display: grid;
7 | grid-template-columns: repeat(3, 240px);
8 | margin-bottom: .8rem;
9 | }
10 |
11 | .itemLabel {
12 | margin-right: 1rem;
13 | font-weight: bold;
14 | }
15 |
16 | .jsonContent {
17 | max-height: 1000px;
18 | overflow: auto;
19 | }
20 |
21 | .tryIt code {
22 | max-height: 1000px !important;
23 | overflow: auto !important;
24 | }
25 |
26 | .notSupported {
27 | display: none;
28 | }
29 |
30 | .tryItCbox {
31 | display: block;
32 | }
33 |
34 | .codeBlockContainer {
35 | position: relative;
36 | }
37 |
38 | .loading {
39 | position: absolute;
40 | right: 8px;
41 | top: 60px;
42 | color: var(--ifm-color-primary);
43 | z-index: 1;
44 | }
45 |
46 | @media screen and (max-width: 1280px) {
47 | .items {
48 | grid-template-columns: repeat(2, 240px);
49 | }
50 | }
51 |
52 | @media screen and (max-width: 480px) {
53 | .tryIt {
54 | display: none;
55 | }
56 |
57 | .notSupported {
58 | display: block;
59 | }
60 | }
--------------------------------------------------------------------------------
/src/components/Highlight.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function Highlight(params) {
4 | return (
5 |
15 | {params.content}
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/src/components/PkceChallenge.js:
--------------------------------------------------------------------------------
1 | function generateRandomString(length) {
2 | let text = "";
3 | const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_~";
4 |
5 | for (let i = 0; i < length; i++) {
6 | text += possible.charAt(Math.floor(Math.random() * possible.length));
7 | }
8 |
9 | return text;
10 | }
11 |
12 | export default function PkceChallenge(){
13 | return generateRandomString(128);
14 | }
--------------------------------------------------------------------------------
/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * Any CSS included here will be global. The classic template
4 | * bundles Infima by default. Infima is a CSS framework designed to
5 | * work well for content-centric websites.
6 | */
7 |
8 | /* You can override the default Infima variables here. */
9 | :root {
10 | --ifm-navbar-background: #361C47;
11 | --collapse-button-bg-color-dark: #f5f6f7 !important;
12 | --ifm-navbar-link-color: #f5f6f7;
13 | --ifm-navbar-link-hover-color: #AD6DD3;
14 | --ifm-color-primary: #8D46B8;
15 | --ifm-color-primary-dark: #7C3EA3;
16 | --ifm-color-primary-darker: #653285;
17 | --ifm-color-primary-darkest: #4F2768;
18 | --ifm-color-primary-light: #AD6DD3;
19 | --ifm-color-primary-lighter: #C291DE;
20 | --ifm-color-primary-lightest: #D3B0E8;
21 | --ifm-button-color: white;
22 | --ifm-code-font-size: 95%;
23 | }
24 |
25 | .docusaurus-highlight-code-line {
26 | background-color: rgba(0, 0, 0, 0.1);
27 | display: block;
28 | margin: 0 calc(-1 * var(--ifm-pre-padding));
29 | padding: 0 var(--ifm-pre-padding);
30 | }
31 |
32 | html[data-theme='dark'] {
33 | --ifm-background-color: #151F2E;
34 | }
35 |
36 | html[data-theme='dark'] .docusaurus-highlight-code-line {
37 | background-color: rgba(0, 0, 0, 0.3);
38 | }
39 |
40 | .navbar,
41 | .navbar-sidebar {
42 | background-color: var(--ifm-navbar-background);
43 | }
44 |
45 | .navbar__brand:hover,
46 | .navbar__brand:active {
47 | color: var(--ifm-navbar-link-color);
48 | }
49 |
50 | .navbar__items .icon {
51 | font-size: 22px;
52 | padding-top: .5rem;
53 | padding-bottom: .5rem;
54 | border-radius: 8px;
55 | transition: .1s;
56 | }
57 |
58 | .navbar__items .icon:hover {
59 | color: white;
60 | transition: .3s;
61 | }
62 |
63 | .navbar__items .icon.fa-patreon:hover {
64 | background-color: #f96854;
65 | }
66 |
67 | .navbar__items .icon.fa-github:hover {
68 | background-color: #6e5494;
69 | }
70 |
71 | .navbar__items .icon.fa-discord:hover {
72 | background-color: #7289DA;
73 | }
74 |
75 | .navbar {
76 | --ifm-menu-color: #DADDE1;
77 | }
78 |
79 | html[data-theme='light'] .footer--dark {
80 | --ifm-footer-background-color: #F4F8F9;
81 | --ifm-footer-color: rgb(28, 30, 33);
82 | --ifm-footer-link-color: var(--ifm-color-primary);
83 | --ifm-footer-title-color: rgb(28, 30, 33);
84 | }
85 |
86 | html[data-theme='dark'] .footer--dark {
87 | --ifm-footer-background-color: #0B1622;
88 | }
89 |
90 | .clean-btn {
91 | color: var(--collapse-button-bg-color-dark);
92 | }
93 |
94 | .get-started-section {
95 | display: flex;
96 | justify-content: center;
97 | align-items: center;
98 | padding: 2rem 0 4rem 0;
99 | width: 100%;
100 | }
101 |
102 | .get-started-section .button {
103 | text-transform: uppercase;
104 | }
105 |
106 | .hero {
107 | background-color: var(--ifm-navbar-background) !important;
108 | }
109 |
110 | .hero__logo {
111 | max-height: 128px;
112 | }
113 |
114 | .hero__subtitle {
115 | margin-top: 36px;
116 | font-weight: 300;
117 | color: white;
118 | }
119 |
120 | .language-http span {
121 | color: rgb(98, 114, 164) !important;
122 | }
123 |
124 | hr {
125 | border-color: var(--ifm-toc-border-color);
126 | }
127 |
128 | .codeBlockTitle_node_modules-\@docusaurus-theme-classic-lib-next-theme-CodeBlock-styles-module {
129 | white-space: nowrap;
130 | overflow: hidden;
131 | text-overflow: ellipsis;
132 | }
--------------------------------------------------------------------------------
/src/pages/anilist.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import Layout from '@theme/Layout';
4 |
5 | export default function Anilist() {
6 |
7 | let user;
8 |
9 | try {
10 | user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
11 |
12 | if (!user) {
13 | window.location.replace('/login');
14 | }
15 | }
16 | catch { }
17 |
18 | useEffect(async () => {
19 | const access_token = location.hash.substring(1).split('=')[1].split('&')[0];
20 | const id = await getAnilistId(access_token);
21 |
22 | await updateUser(access_token, id);
23 | }, []);
24 |
25 | const getAnilistId = async (token) => {
26 | try {
27 | const response = await fetch(`https://graphql.anilist.co`, {
28 | method: 'POST',
29 | headers: {
30 | 'Authorization': `Bearer ${token}`,
31 | 'Accept': 'application/json',
32 | 'Content-Type': 'application/json'
33 | },
34 | body: JSON.stringify({
35 | query: `
36 | query {
37 | Viewer {
38 | id
39 | name
40 | }
41 | }
42 | `
43 | })
44 | });
45 |
46 | const body = await response.json();
47 |
48 | return body.data.Viewer.id;
49 | }
50 | catch {
51 | window.location.replace('/profile#trackers');
52 | }
53 | }
54 |
55 | const updateUser = async (token, id) => {
56 | const response = await fetch(`${process.env.API_URL}/v1/user`, {
57 | method: 'POST',
58 | headers: {
59 | 'Authorization': `Bearer ${user.access_token}`,
60 | 'Accept': 'application/json',
61 | 'Content-Type': 'application/json'
62 | },
63 | body: JSON.stringify({
64 | id: user.id,
65 | anilist_id: id,
66 | anilist_token: token
67 | })
68 | });
69 |
70 | const body = await response.json();
71 |
72 | if (body.status_code === 200) {
73 | user.has_anilist = body.data.has_anilist;
74 | window.localStorage.setItem('AUTH_USER', JSON.stringify(user));
75 | }
76 |
77 | window.location.replace('/profile#trackers');
78 | }
79 |
80 | return (
81 |
82 |
83 |
84 | Please wait...
85 |
86 |
87 |
88 | );
89 | }
--------------------------------------------------------------------------------
/src/pages/developer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import Layout from '@theme/Layout';
4 | import styles from './developer.module.css';
5 |
6 | export default function Developer() {
7 |
8 | let user;
9 |
10 | const [sideItems, setSideItems] = useState([]);
11 | const [panels, setPanels] = useState([]);
12 |
13 | const [id, setId] = useState(-1);
14 | const [accessToken, setAccessToken] = useState('');
15 |
16 | const [cId, setCId] = useState(-1);
17 | const [name, setName] = useState('');
18 | const [redirectUri, setRedirectUri] = useState('');
19 | const [clientId, setClientId] = useState('');
20 |
21 | const [clients, setClients] = useState([]);
22 |
23 | const [loading, setLoading] = useState(false);
24 |
25 | useEffect(async () => {
26 | try {
27 | setSideItems(document.getElementsByClassName(styles.sideItem));
28 | setPanels(document.getElementsByClassName(styles.panel));
29 |
30 | user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
31 |
32 | setId(user.id);
33 | setAccessToken(user.access_token);
34 | }
35 | catch {
36 | window.location.replace('/login');
37 | }
38 | }, []);
39 |
40 | useEffect(async () => {
41 | await loadClients();
42 | }, [id]);
43 |
44 | useEffect(() => {
45 | for (let i = 0; i < sideItems.length; i++) {
46 | sideItems[i].removeEventListener('click', onSideItemClick, true);
47 | sideItems[i].addEventListener('click', onSideItemClick, true);
48 | }
49 |
50 | const panelId = location.hash.substr(1);
51 | if (panelId) {
52 | selectSideItem(panelId);
53 | selectPanel(panelId);
54 | }
55 | }, [sideItems, panels]);
56 |
57 | const loadClients = async () => {
58 | const response = await fetch(`${process.env.API_URL}/v1/oauth_client?user_id=${id}`, {
59 | method: 'GET',
60 | headers: {
61 | 'Accept': 'application/json',
62 | 'Content-Type': 'application/json'
63 | }
64 | });
65 |
66 | const body = await response.json();
67 |
68 | if (body.status_code !== 200) {
69 | return;
70 | }
71 |
72 | const cls = [];
73 |
74 | for (let i = 0; i < body.data.documents.length; i++) {
75 | const c = body.data.documents[i];
76 | cls.push((
77 |
78 | {c.name}
79 |
80 | Id
81 | {c.client_id}
82 |
83 |
84 | Redirect URI
85 | {c.redirect_uri}
86 |
87 | {loading ? (
88 |
89 | ) : (
90 |
92 | )}
93 |
103 |
104 | ));
105 | }
106 |
107 | setClients(cls);
108 | }
109 |
110 | const onSideItemClick = (e) => {
111 | const panelId = e.target.getAttribute('data-panelid');
112 | selectSideItem(panelId);
113 | selectPanel(panelId);
114 | }
115 |
116 | const selectSideItem = (panelId) => {
117 | for (let i = 0; i < sideItems.length; i++) {
118 | if (sideItems[i].getAttribute('data-panelid') === panelId) {
119 | sideItems[i].classList.add(styles.selected);
120 | }
121 | else {
122 | sideItems[i].classList.remove(styles.selected);
123 | }
124 | }
125 | }
126 |
127 | const selectPanel = (panelId) => {
128 | for (let i = 0; i < panels.length; i++) {
129 | if (panels[i].getAttribute('data-panelid') === panelId) {
130 | panels[i].classList.add(styles.visible);
131 | }
132 | else {
133 | panels[i].classList.remove(styles.visible);
134 | }
135 | }
136 | }
137 |
138 | const onAdd = async () => {
139 | setLoading(true);
140 |
141 | const errors = document.getElementById('add-errors');
142 |
143 | errors.innerHTML = '';
144 |
145 | if (!name) {
146 | errors.innerHTML += 'Name must not be empty';
147 | setLoading(false);
148 | return;
149 | }
150 |
151 | if (!redirectUri) {
152 | errors.innerHTML += 'Redirect URI must not be empty';
153 | setLoading(false);
154 | return;
155 | }
156 |
157 | const response = await fetch(`${process.env.API_URL}/v1/oauth_client`, {
158 | method: 'PUT',
159 | headers: {
160 | 'Authorization': `Bearer ${accessToken}`,
161 | 'Accept': 'application/json',
162 | 'Content-Type': 'application/json'
163 | },
164 | body: JSON.stringify({
165 | name: name,
166 | redirect_uri: redirectUri
167 | })
168 | });
169 |
170 | const body = await response.json();
171 |
172 | if (body.status_code === 200) {
173 | await loadClients();
174 |
175 | selectSideItem('list');
176 | selectPanel('list');
177 |
178 | console.log(`This is your ${name} client's secret.\nYou won't be able to copy it in future times.\nSave it in a safe place!`);
179 | console.log(body.data.client_secret);
180 | alert('Check your console to copy the client\'s secret [F12]');
181 |
182 | setLoading(false);
183 | }
184 | else {
185 | setLoading(false);
186 | errors.innerHTML += body.data;
187 | return;
188 | }
189 | }
190 |
191 | const onEdit = async () => {
192 | setLoading(true);
193 |
194 | const errors = document.getElementById('edit-errors');
195 |
196 | errors.innerHTML = '';
197 |
198 | if (!name) {
199 | errors.innerHTML += 'Name must not be empty';
200 | setLoading(false);
201 | return;
202 | }
203 |
204 | if (!redirectUri) {
205 | errors.innerHTML += 'Redirect URI must not be empty';
206 | setLoading(false);
207 | return;
208 | }
209 |
210 | const response = await fetch(`${process.env.API_URL}/v1/oauth_client`, {
211 | method: 'POST',
212 | headers: {
213 | 'Authorization': `Bearer ${accessToken}`,
214 | 'Accept': 'application/json',
215 | 'Content-Type': 'application/json'
216 | },
217 | body: JSON.stringify({
218 | id: cId,
219 | user_id: id,
220 | client_id: clientId,
221 | name: name,
222 | redirect_uri: redirectUri
223 | })
224 | });
225 |
226 | const body = await response.json();
227 |
228 | if (body.status_code === 200) {
229 | await loadClients();
230 |
231 | selectSideItem('list');
232 | selectPanel('list');
233 |
234 | setLoading(false);
235 | }
236 | else {
237 | setLoading(false);
238 | errors.innerHTML += body.data;
239 | return;
240 | }
241 | }
242 |
243 | const onDelete = async (id) => {
244 | setLoading(true);
245 |
246 | const response = await fetch(`${process.env.API_URL}/v1/oauth_client/${id}`, {
247 | method: 'DELETE',
248 | headers: {
249 | 'Authorization': `Bearer ${accessToken}`,
250 | 'Accept': 'application/json',
251 | 'Content-Type': 'application/json'
252 | }
253 | });
254 |
255 | const body = await response.json();
256 |
257 | if (body.status_code === 200) {
258 | await loadClients();
259 |
260 | selectSideItem('list');
261 | selectPanel('list');
262 |
263 | setLoading(false);
264 | }
265 | else {
266 | setLoading(false);
267 | alert(body.data);
268 | return;
269 | }
270 | }
271 |
272 | return (
273 |
274 |
275 |
276 |
277 |
278 | Menu
279 |
281 | List
282 |
283 | {
286 | setName('');
287 | setRedirectUri('');
288 | }}>
289 | Add
290 |
291 |
292 |
293 |
295 | List
296 | {clients.length === 0 ? (
297 | No clients found
298 | ) : (
299 |
300 | {clients}
301 |
302 | )}
303 |
304 |
306 | Add
307 |
330 |
331 |
333 | Edit
334 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | );
364 | }
--------------------------------------------------------------------------------
/src/pages/developer.module.css:
--------------------------------------------------------------------------------
1 | .sideItem {
2 | margin-bottom: .5rem;
3 | }
4 |
5 | .sideItem:hover {
6 | cursor: pointer;
7 | }
8 |
9 | .sideItem:hover,
10 | .sideItem:active,
11 | .sideItem.selected {
12 | color: var(--ifm-color-primary);
13 | }
14 |
15 | .panel {
16 | display: none;
17 | }
18 |
19 | .panel.visible {
20 | display: block;
21 | }
22 |
23 | .client {
24 | padding: 16px;
25 | background-color: #0B1622;
26 | border-radius: 6px;
27 | box-sizing: border-box;
28 | }
29 |
30 | .client:not(:first-child) {
31 | margin-top: 1rem;
32 | }
33 |
34 | .client::after {
35 | content: '';
36 | display: table;
37 | clear: both;
38 | }
39 |
40 | .client h4 {
41 | color: var(--ifm-color-primary);
42 | }
43 |
44 | .client button {
45 | float: right;
46 | margin-top: 1.6rem;
47 | padding: 12px 24px;
48 | }
49 |
50 | .client button:not(:first-child) {
51 | margin-left: 1rem;
52 | }
53 |
54 | .clientRow {
55 | display: flex;
56 | justify-content: space-between;
57 | }
58 |
59 | .clientRow span {
60 | font-weight: 500;
61 | }
62 |
63 | .inputLabel:not(:first-child) {
64 | margin-top: 1rem;
65 | }
66 |
67 | .inputLabel {
68 | margin-bottom: .3rem;
69 | }
70 |
71 | .errors {
72 | margin: 12px 0;
73 | color: red;
74 | text-align: left;
75 | font-weight: 300;
76 | }
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import clsx from 'clsx';
3 | import Layout from '@theme/Layout';
4 | import Link from '@docusaurus/Link';
5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
6 | import styles from './index.module.css';
7 |
8 | function HomepageHeader() {
9 | const { siteConfig } = useDocusaurusContext();
10 | return (
11 |
12 |
13 |
14 | {siteConfig.tagline}
15 |
16 |
17 | );
18 | }
19 |
20 | export default function Home() {
21 | const { siteConfig } = useDocusaurusContext();
22 |
23 | const wrappers = [
24 | {
25 | name: 'AniAPI.NET',
26 | author: 'Dazorn96',
27 | github: 'https://github.com/AniAPI-Team/AniAPI.NET',
28 | language: 'C#',
29 | bgColor: '#783bd2',
30 | fgColor: '#fff'
31 | },
32 | {
33 | name: 'AniAPI.js',
34 | author: 'MattPlays',
35 | github: 'https://github.com/MattPlays/AniAPI.js',
36 | language: 'JS',
37 | bgColor: '#f7df1e',
38 | fgColor: '#000'
39 | },
40 | {
41 | name: 'AniAPI Java SDK',
42 | author: 'censodev',
43 | github: 'https://github.com/censodev/aniapi-java-sdk',
44 | language: 'Java',
45 | bgColor: '#68BD45',
46 | fgColor: '#fff'
47 | }
48 | /*
49 | REFER TO THIS EXAMPLE. PLEASE LEAVE IT HERE, DO NOT CLEAR IT!
50 | {
51 | name: 'the wrapper name',
52 | author: 'the wrapper author',
53 | github: 'the wrapper repository github url',
54 | language: 'the wrapper programming language',
55 | bgColor: 'refer to https://brandcolors.net/ to find the best color',
56 | fgColor: 'an high-contrast color in order to read the content'
57 | }
58 | */
59 | ];
60 |
61 | const apps = [
62 | {
63 | name: 'Hibike',
64 | description: 'A simple preview anisong website',
65 | preview: 'https://github.com/Nemure231/My-Docs/blob/main/Upload/Hibike.png?raw=true',
66 | url: 'https://hibike2.herokuapp.com'
67 | }
68 | /*
69 | REFER TO THIS EXAMPLE. PLEASE LEAVE IT HERE, DO NOT CLEAR IT!
70 | {
71 | name: 'the application name',
72 | description: 'a short description of the application',
73 | preview: 'an image of the application preview',
74 | url: 'url to download/use the app'
75 | }
76 | */
77 | ];
78 |
79 | return (
80 |
83 |
84 |
85 |
86 |
87 |
88 | AniAPI is an open-source REST API for anime streaming lovers.
89 | You can focus on making your app while we care about all the things.
90 |
91 |
95 | Learn more
96 |
97 |
100 | Get started
101 |
102 |
103 |
104 |
105 | Use the language you love
106 |
107 | {wrappers.length === 0 && (
108 | No wrappers for the moment.
109 | )}
110 | {wrappers.map((w, i) => {
111 | return (
112 |
120 | {w.name}
121 |
122 | {w.author} • {w.language}
123 |
124 |
125 | );
126 | })}
127 |
128 |
129 | Help contribute by making a wrapper in your favourite programming language.
130 | Made a wrapper? Edit this page!
131 |
132 |
133 |
134 |
135 | Apps using AniAPI
136 |
137 | {apps.length === 0 && (
138 | No applications for now.
139 | )}
140 | {apps.map((a, i) => {
141 | return (
142 |
149 |
150 | {a.name}
151 | {a.description}
152 |
153 |
154 | );
155 | })}
156 |
157 |
161 | Add your app
162 |
163 |
164 |
165 |
166 | Join the community
167 |
169 |
172 |
173 |
174 |
175 |
176 | Our supporters
177 | AniAPI is able to reach out more developers with good performances thanks to our patrons...
178 |
179 |
180 | Kawaii's
181 | Nick Geel
182 |
183 |
184 | Onii/Onee-chan's
185 | -
186 |
187 |
188 | Senpai's
189 | -
190 |
191 |
192 |
193 |
194 | Sensei's
195 | -
196 |
197 |
198 | Deredere's
199 | -
200 |
201 |
202 | Waifu's
203 | -
204 |
205 |
206 |
210 | Support us
211 |
212 |
213 |
214 |
215 |
216 | );
217 | }
218 |
--------------------------------------------------------------------------------
/src/pages/index.module.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 |
3 | /**
4 | * CSS files with the .module.css suffix will be treated as CSS modules
5 | * and scoped locally.
6 | */
7 |
8 | .indexMain {
9 | display: block !important;
10 | margin-top: 0 !important;
11 | }
12 |
13 | .heroBanner {
14 | padding: 4rem 0;
15 | text-align: center;
16 | position: relative;
17 | overflow: hidden;
18 | }
19 |
20 | @media screen and (max-width: 966px) {
21 | .heroBanner {
22 | padding: 2rem;
23 | }
24 | }
25 |
26 | .buttons {
27 | display: flex;
28 | align-items: center;
29 | justify-content: center;
30 | }
31 |
32 | .homeSection {
33 | padding: 6rem var(--ifm-spacing-horizontal);
34 | }
35 |
36 | .homeSection h1 {
37 | text-align: center;
38 | }
39 |
40 | .introSection a:last-child {
41 | margin-left: 1.5rem;
42 | }
43 |
44 | .introSection,
45 | .wrapperSection,
46 | .showcaseSection,
47 | .communitySection,
48 | .supportersSection {
49 | display: flex;
50 | flex-direction: column;
51 | align-items: center;
52 | justify-content: center;
53 | width: 100%;
54 | text-align: center;
55 | }
56 |
57 | .wrapperSection {
58 | background-color: #0B1622;
59 | }
60 |
61 | .wrappers {
62 | margin: 5rem 0;
63 | display: flex;
64 | flex-wrap: wrap;
65 | }
66 |
67 | .wrapper {
68 | margin: .5rem;
69 | padding: 3rem 4rem;
70 | border-radius: .4rem;
71 | text-decoration: none !important;
72 | }
73 |
74 | .wrapper h2 {
75 | margin: 0;
76 | }
77 |
78 | .wrapper a {
79 | color: inherit;
80 | }
81 |
82 | .apps {
83 | margin: 3rem 0;
84 | display: flex;
85 | flex-wrap: wrap;
86 | justify-content: center;
87 | }
88 |
89 | .app {
90 | display: flex;
91 | align-items: flex-end;
92 | margin: .5rem;
93 | width: 280px;
94 | height: 420px;
95 | background-size: cover;
96 | background-position: center;
97 | border-radius: .4rem .4rem .5rem .5rem;
98 | text-decoration: none !important;
99 | }
100 |
101 | .appInfo {
102 | padding: 2rem;
103 | width: 100%;
104 | background-color: #0B1622;
105 | border-bottom-left-radius: .4rem;
106 | border-bottom-right-radius: .4rem;
107 | }
108 |
109 | .appInfo p {
110 | color: white;
111 | font-size: 80%;
112 | }
113 |
114 | .communitySection {
115 | background-color: var(--ifm-navbar-background);
116 | }
117 |
118 | .communitySection h1 {
119 | margin-bottom: 3rem;
120 | }
121 |
122 | .communitySection svg {
123 | width: 32px;
124 | height: 32px;
125 | }
126 |
127 | .supportersTiers {
128 | display: flex;
129 | justify-content: center;
130 | }
131 |
132 | .supportersTier {
133 | margin: 3rem 5rem;
134 | text-align: left;
135 | }
136 |
137 | @media screen and (max-width:400px) {
138 |
139 | .introSection a {
140 | width: 100%;
141 | }
142 |
143 | .introSection a:last-child {
144 | margin-left: 0;
145 | margin-top: 1rem;
146 | }
147 |
148 | .wrapper {
149 | width: 100%;
150 | }
151 |
152 | }
153 |
--------------------------------------------------------------------------------
/src/pages/login.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import Layout from '@theme/Layout';
4 | import Link from '@docusaurus/Link';
5 | import styles from './login.module.css';
6 |
7 | export default function Login() {
8 |
9 | const SITE_KEY = '6Lf98EgbAAAAAHSXXQIrsL-yByCMxOHsukzOqvHV';
10 |
11 | const [email, setEmail] = useState('');
12 | const [password, setPassword] = useState('');
13 |
14 | const [loading, setLoading] = useState(false);
15 |
16 | try {
17 | const user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
18 |
19 | if (user) {
20 | window.location.replace('/profile');
21 | }
22 | }
23 | catch { }
24 |
25 | useEffect(() => {
26 | const loadScript = (id, url, callback) => {
27 | const exists = document.getElementById(id);
28 |
29 | if (!exists) {
30 | var script = document.createElement('script');
31 | script.type = 'text/javascript';
32 | script.src = url;
33 | script.id = id;
34 |
35 | script.onload = () => {
36 | if (callback) {
37 | callback();
38 | }
39 | }
40 |
41 | document.body.appendChild(script);
42 | }
43 |
44 | if (exists && callback) {
45 | callback();
46 | }
47 | }
48 |
49 | loadScript('recaptcha-key', `https://www.google.com/recaptcha/api.js?render=${SITE_KEY}`, () => {
50 |
51 | });
52 | }, []);
53 |
54 | const onEmailKeyPress = (e) => {
55 | if (e.which === 13) {
56 | document.getElementById('password').focus();
57 | }
58 |
59 | setEmail(email.toLowerCase());
60 | }
61 |
62 | const onPasswordKeyPress = (e) => {
63 | if (e.which === 13) {
64 | document.getElementById('login').click();
65 | }
66 | }
67 |
68 | const onLogin = (e) => {
69 | setLoading(true);
70 |
71 | const errors = document.getElementById('errors');
72 |
73 | errors.innerHTML = '';
74 |
75 | if (!email) {
76 | errors.innerHTML += 'Email must not be empty';
77 | setLoading(false);
78 | return;
79 | }
80 |
81 | if (!password) {
82 | errors.innerHTML += 'Password must not be empty';
83 | setLoading(false);
84 | return;
85 | }
86 |
87 | window.grecaptcha.ready(() => {
88 | window.grecaptcha.execute(SITE_KEY, { action: 'submit' }).then(async (token) => {
89 | const response = await fetch(`${process.env.API_URL}/v1/auth`, {
90 | method: 'POST',
91 | headers: {
92 | 'Accept': 'application/json',
93 | 'Content-Type': 'application/json'
94 | },
95 | body: JSON.stringify({
96 | email: email,
97 | password: password,
98 | g_recaptcha_response: token
99 | })
100 | });
101 |
102 | const body = await response.json();
103 |
104 | if (body.status_code === 200) {
105 | window.localStorage.setItem('AUTH_USER', JSON.stringify(body.data));
106 | window.location.reload();
107 | }
108 | else {
109 | errors.innerHTML += body.data;
110 | setLoading(false);
111 | return;
112 | }
113 | });
114 | });
115 | }
116 |
117 | return (
118 |
119 |
120 |
121 |
122 |
146 |
149 | Create an account
150 |
151 |
152 |
153 |
154 | );
155 | }
--------------------------------------------------------------------------------
/src/pages/login.module.css:
--------------------------------------------------------------------------------
1 | .loginMain {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | }
6 |
7 | .form {
8 | padding: 24px;
9 | width: 300px;
10 | text-align: center;
11 | }
12 |
13 | form {
14 | margin-top: 22px;
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | input {
20 | margin: 8px 0;
21 | padding: 12px 18px;
22 | background-color: #151f2e;
23 | border: 1px solid rgba(255, 255, 255, .1);
24 | border-radius: 6px;
25 | color: rgba(255, 255, 255, .6);
26 | outline: none;
27 | transition: .3s;
28 | }
29 |
30 | input:focus, input:active {
31 | border-color: rgba(255, 255, 255, .3);
32 | color: white;
33 | transition: .3s;
34 | }
35 |
36 | button {
37 | margin-top: 8px;
38 | padding: 12px 0;
39 | background-color: #8d46b8;
40 | border: none;
41 | border-radius: 6px;
42 | color: white;
43 | text-transform: uppercase;
44 | font-weight: 600;
45 | transition: .3s;
46 | }
47 |
48 | button:hover, button:active {
49 | cursor: pointer;
50 | transition: .3s;
51 | }
52 |
53 | .errors {
54 | margin: 12px 0;
55 | color: red;
56 | text-align: left;
57 | font-weight: 300;
58 | }
59 |
60 | .signup {
61 | display: block;
62 | margin-top: 12px;
63 | text-align: center;
64 | }
--------------------------------------------------------------------------------
/src/pages/mal.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import Layout from '@theme/Layout';
4 |
5 | export default function Mal() {
6 |
7 | let user;
8 |
9 | try {
10 | user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
11 |
12 | if (!user) {
13 | window.location.replace('/login');
14 | }
15 | }
16 | catch { }
17 |
18 | useEffect(async () => {
19 | const query = Object.fromEntries(new URLSearchParams(window.location.search).entries());
20 |
21 | const challenge = window.localStorage.getItem('MAL_CHALLENGE');
22 |
23 | if (challenge !== query.state) {
24 | window.location.replace('/login');
25 | }
26 |
27 | const user = await getToken(query.code, challenge);
28 |
29 | await updateUser(user.token, user.id);
30 | }, []);
31 |
32 | const getToken = async (code, code_verifier) => {
33 | try {
34 | const payload = {
35 | client_id: process.env.MAL_CLIENTID,
36 | client_secret: process.env.MAL_CLIENTSECRET,
37 | grant_type: 'authorization_code',
38 | code: code,
39 | code_verifier: code_verifier
40 | };
41 |
42 | var formBody = [];
43 | for (var p in payload) {
44 | formBody.push(`${encodeURIComponent(p)}=${encodeURIComponent(payload[p])}`);
45 | }
46 | formBody = formBody.join('&');
47 |
48 | const response = await fetch('https://cors-anywhere.herokuapp.com/myanimelist.net/v1/oauth2/token', {
49 | method: 'POST',
50 | headers: {
51 | 'Accept': 'application/json',
52 | 'Content-Type': 'application/x-www-form-urlencoded'
53 | },
54 | body: formBody
55 | });
56 |
57 | const body = await response.json();
58 |
59 | const id = await getUserId(body.access_token);
60 |
61 | return {
62 | id: id,
63 | token: body.refresh_token
64 | };
65 | }
66 | catch (ex) {
67 | window.location.replace('/profile#trackers');
68 | }
69 | }
70 |
71 | const getUserId = async (access_token) => {
72 | try{
73 | const response = await fetch('https://cors-anywhere.herokuapp.com/api.myanimelist.net/v2/users/@me', {
74 | method: 'GET',
75 | headers: {
76 | 'Authorization': `Bearer ${access_token}`,
77 | 'Accept': 'application/json',
78 | 'Content-Type': 'application/json'
79 | }
80 | });
81 |
82 | const body = await response.json();
83 |
84 | return body.id;
85 | }
86 | catch (ex) {
87 | window.location.replace('/profile#trackers');
88 | }
89 | }
90 |
91 | const updateUser = async (token, id) => {
92 | const response = await fetch(`${process.env.API_URL}/v1/user`, {
93 | method: 'POST',
94 | headers: {
95 | 'Authorization': `Bearer ${user.access_token}`,
96 | 'Accept': 'application/json',
97 | 'Content-Type': 'application/json'
98 | },
99 | body: JSON.stringify({
100 | id: user.id,
101 | mal_id: id,
102 | mal_token: token
103 | })
104 | });
105 |
106 | const body = await response.json();
107 |
108 | if (body.status_code === 200) {
109 | user.has_mal = body.data.has_mal;
110 | window.localStorage.setItem('AUTH_USER', JSON.stringify(user));
111 | }
112 |
113 | window.location.replace('/profile#trackers');
114 | }
115 |
116 | return (
117 |
118 |
119 |
120 | Please wait...
121 |
122 |
123 |
124 | );
125 | }
--------------------------------------------------------------------------------
/src/pages/profile.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import Layout from '@theme/Layout';
4 | import CodeBlock from '@theme/CodeBlock';
5 | import styles from './profile.module.css';
6 |
7 | import PkceChallenge from '../components/PkceChallenge';
8 |
9 | export default function Profile() {
10 | const anilistURL = `https://anilist.co/api/v2/oauth/authorize?client_id=${process.env.ANILIST_CLIENTID}&response_type=token`;
11 |
12 | const [malURL, setMalURL] = useState('');
13 |
14 | let sideItems;
15 | let panels;
16 | let user;
17 |
18 | const [id, setId] = useState(-1);
19 | const [username, setUsername] = useState('');
20 | const [accessToken, setAccessToken] = useState('');
21 |
22 | const [password, setPassword] = useState('');
23 | const [passwordConfirm, setPasswordConfirm] = useState('');
24 | const [gender, setGender] = useState(0);
25 | const [locale, setLocale] = useState('');
26 | const [avatar, setAvatar] = useState('');
27 |
28 | const [locales, setLocales] = useState([]);
29 |
30 | const [loading, setLoading] = useState(false);
31 |
32 | useEffect(async () => {
33 | try {
34 | sideItems = document.getElementsByClassName(styles.sideItem);
35 | panels = document.getElementsByClassName(styles.panel);
36 |
37 | user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
38 |
39 | setId(user.id);
40 | setUsername(user.username);
41 | setAccessToken(user.access_token);
42 | setLocale(user.localization);
43 | setGender(user.gender);
44 | setAvatar(user.avatar_tracker ? user.avatar_tracker : 'none');
45 |
46 | await loadLocalizations();
47 | await loadMalURL();
48 | }
49 | catch (ex) {
50 | window.location.replace('/login');
51 | }
52 |
53 | for (let i = 0; i < sideItems.length; i++) {
54 | sideItems[i].addEventListener('click', onSideItemClick, true);
55 | }
56 |
57 | const panelId = location.hash.substr(1);
58 | if (panelId) {
59 | selectSideItem(panelId);
60 | selectPanel(panelId);
61 | }
62 |
63 | if (user.has_anilist) {
64 | document.getElementById('anilist-tracker').classList.add(styles.trackerActive);
65 | }
66 | if (user.has_mal) {
67 | document.getElementById('mal-tracker').classList.add(styles.trackerActive);
68 | }
69 | }, []);
70 |
71 | const loadMalURL = async () => {
72 | const challenge = PkceChallenge();
73 | setMalURL(`https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=${process.env.MAL_CLIENTID}&state=${challenge}&code_challenge=${challenge}&code_challenge_method=plain`);
74 | window.localStorage.setItem('MAL_CHALLENGE', challenge);
75 | }
76 |
77 | const loadLocalizations = async () => {
78 | const version = await fetch(`${process.env.API_URL}/v1/resources`, {
79 | method: 'GET',
80 | headers: {
81 | 'Accept': 'application/json',
82 | 'Content-Type': 'application/json'
83 | }
84 | });
85 |
86 | const versionBody = await version.json();
87 |
88 | const response = await fetch(`${process.env.API_URL}/v1/resources/${versionBody.data}/1`, {
89 | method: 'GET',
90 | headers: {
91 | 'Accept': 'application/json',
92 | 'Content-Type': 'application/json'
93 | }
94 | });
95 |
96 | const body = await response.json();
97 |
98 | const ls = [];
99 |
100 | for (let i = 0; i < body.data.localizations.length; i++) {
101 | const l = body.data.localizations[i];
102 | ls.push();
103 | }
104 |
105 | setLocales(ls);
106 | }
107 |
108 | const onSideItemClick = (e) => {
109 | const panelId = e.target.getAttribute('data-panelid');
110 | selectSideItem(panelId);
111 | selectPanel(panelId);
112 | }
113 |
114 | const selectSideItem = (panelId) => {
115 | for (let i = 0; i < sideItems.length; i++) {
116 | if (sideItems[i].getAttribute('data-panelid') === panelId) {
117 | sideItems[i].classList.add(styles.selected);
118 | }
119 | else {
120 | sideItems[i].classList.remove(styles.selected);
121 | }
122 | }
123 | }
124 |
125 | const selectPanel = (panelId) => {
126 | for (let i = 0; i < panels.length; i++) {
127 | if (panels[i].getAttribute('data-panelid') === panelId) {
128 | panels[i].classList.add(styles.visible);
129 | }
130 | else {
131 | panels[i].classList.remove(styles.visible);
132 | }
133 | }
134 | }
135 |
136 | const onSaveEdit = async () => {
137 | setLoading(true);
138 |
139 | const success = document.getElementById('success');
140 | const errors = document.getElementById('errors');
141 |
142 | let payload = {};
143 |
144 | success.innerHTML = '';
145 | errors.innerHTML = '';
146 |
147 | if (password) {
148 | if (password !== passwordConfirm) {
149 | errors.innerHTML += 'Passwords must match';
150 | setLoading(false);
151 | return;
152 | }
153 |
154 | payload.password = password;
155 | }
156 |
157 | if (locale) {
158 | payload.localization = locale;
159 | }
160 |
161 | if (gender !== undefined && gender !== null) {
162 | payload.gender = gender;
163 | }
164 |
165 | payload.avatar_tracker = avatar;
166 | payload.id = id;
167 |
168 | const response = await fetch(`${process.env.API_URL}/v1/user`, {
169 | method: 'POST',
170 | headers: {
171 | 'Authorization': `Bearer ${accessToken}`,
172 | 'Accept': 'application/json',
173 | 'Content-Type': 'application/json'
174 | },
175 | body: JSON.stringify(payload)
176 | });
177 |
178 | const body = await response.json();
179 |
180 | if (body.status_code === 200) {
181 | let user = JSON.parse(window.localStorage.getItem('AUTH_USER'));
182 |
183 | user.gender = gender;
184 | user.localization = locale;
185 | user.avatar_tracker = avatar;
186 |
187 | window.localStorage.setItem('AUTH_USER', JSON.stringify(user));
188 |
189 | success.innerHTML = 'Saved';
190 | setLoading(false);
191 | }
192 | else {
193 | setLoading(false);
194 | errors.innerHTML += body.data;
195 | return;
196 | }
197 | }
198 |
199 | const onDeveloper = () => {
200 | window.location.replace('/developer');
201 | }
202 |
203 | const onLogout = () => {
204 | window.localStorage.removeItem('AUTH_USER');
205 | window.location.replace('/');
206 | }
207 |
208 | return (
209 |
210 |
211 |
212 |
213 |
214 | Menu
215 |
217 | JWT
218 |
219 |
221 | Edit
222 |
223 |
225 | Trackers
226 |
227 |
229 | Developer
230 |
231 |
233 | Logout
234 |
235 |
236 |
237 |
239 | JWT
240 |
241 | Hi {username}, you can find your JSON Web Token right below:
242 |
243 | {accessToken}
244 |
245 | Remember to follow Authentication guidelines to use the token.
246 |
247 |
248 |
250 | Edit
251 |
332 |
333 |
335 | Trackers
336 |
337 |
340 |
341 |
342 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 | );
355 | }
--------------------------------------------------------------------------------
/src/pages/profile.module.css:
--------------------------------------------------------------------------------
1 | .sideItem {
2 | margin-bottom: .5rem;
3 | }
4 |
5 | .sideItem:hover {
6 | cursor: pointer;
7 | }
8 |
9 | .sideItem:hover,
10 | .sideItem:active,
11 | .sideItem.selected {
12 | color: var(--ifm-color-primary);
13 | }
14 |
15 | .panel {
16 | display: none;
17 | }
18 |
19 | .panel.visible {
20 | display: block;
21 | }
22 |
23 | .trackers {
24 | display: flex;
25 | }
26 |
27 | .tracker {
28 | position: relative;
29 | display: block;
30 | margin-right: 12px;
31 | padding: 12px;
32 | border-radius: 5px;
33 | }
34 |
35 | .tracker img {
36 | display: block;
37 | margin: 0 auto;
38 | width: 48px;
39 | height: 48px;
40 | border-radius: 24px;
41 | }
42 |
43 | .anilist {
44 | background-color: #19212d;
45 | }
46 |
47 | .mal {
48 | background-color: #2e51a2;
49 | }
50 |
51 | .trackerActive::before {
52 | position: absolute;
53 | top: 8px;
54 | right: 8px;
55 | width: 12px;
56 | height: 12px;
57 | background-color: #7bd555;
58 | border-radius: 6px;
59 | content: '';
60 | }
61 |
62 | .trackerDisabled {
63 | filter: grayscale(1);
64 | opacity: .6;
65 | user-select: none;
66 | pointer-events: none;
67 | }
68 |
69 | .inputLabel:not(:first-child) {
70 | margin-top: 1rem;
71 | }
72 |
73 | .inputLabel {
74 | margin-bottom: .3rem;
75 | }
76 |
77 | .genres {
78 | /*margin-bottom: 1.5rem;*/
79 | }
80 |
81 | .genres input,
82 | .avatar input {
83 | margin-left: .4rem;
84 | margin-right: 2rem;
85 | }
86 |
87 | .avatar {
88 | margin-bottom: 1.5rem;
89 | }
90 |
91 | .errors {
92 | margin: 12px 0;
93 | color: red;
94 | text-align: left;
95 | font-weight: 300;
96 | }
97 |
98 | .success {
99 | margin: 12px 0;
100 | color: greenyellow;
101 | text-align: left;
102 | font-weight: 300;
103 | }
104 |
105 | select {
106 | margin: 8px 0;
107 | padding: 12px 18px;
108 | background-color: #151f2e;
109 | border: 1px solid rgba(255, 255, 255, .1);
110 | border-radius: 6px;
111 | color: rgba(255, 255, 255, .6);
112 | outline: none;
113 | transition: .3s;
114 | }
115 |
116 | select:focus, select:active {
117 | border-color: rgba(255, 255, 255, .3);
118 | color: white;
119 | transition: .3s;
120 | }
--------------------------------------------------------------------------------
/src/pages/signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useEffect, useState } from 'react';
3 | import Layout from '@theme/Layout';
4 | import Link from '@docusaurus/Link';
5 | import styles from './signup.module.css';
6 |
7 | export default function Signup() {
8 |
9 | const SITE_KEY = '6Lf98EgbAAAAAHSXXQIrsL-yByCMxOHsukzOqvHV';
10 |
11 | const [email, setEmail] = useState('');
12 | const [username, setUsername] = useState('');
13 | const [password, setPassword] = useState('');
14 | const [passwordConfirm, setPasswordConfirm] = useState('');
15 | const [completed, setCompleted] = useState(false);
16 |
17 | const [loading, setLoading] = useState(false);
18 |
19 | useEffect(() => {
20 | const loadScript = (id, url, callback) => {
21 | const exists = document.getElementById(id);
22 |
23 | if (!exists) {
24 | var script = document.createElement('script');
25 | script.type = 'text/javascript';
26 | script.src = url;
27 | script.id = id;
28 |
29 | script.onload = () => {
30 | if (callback) {
31 | callback();
32 | }
33 | }
34 |
35 | document.body.appendChild(script);
36 | }
37 |
38 | if (exists && callback) {
39 | callback();
40 | }
41 | }
42 |
43 | loadScript('recaptcha-key', `https://www.google.com/recaptcha/api.js?render=${SITE_KEY}`, () => {
44 |
45 | });
46 | }, []);
47 |
48 | const onEmailKeyPress = (e) => {
49 | if (e.which === 13) {
50 | document.getElementById('username').focus();
51 | }
52 | }
53 |
54 | const onUsernameKeyPress = (e) => {
55 | if (e.which === 13) {
56 | document.getElementById('password').focus();
57 | }
58 | }
59 |
60 | const onPasswordKeyPress = (e) => {
61 | if (e.which === 13) {
62 | document.getElementById('password-confirm').focus();
63 | }
64 | }
65 |
66 | const onPasswordConfirmKeyPress = (e) => {
67 | if (e.which === 13) {
68 | document.getElementById('signup').click();
69 | }
70 | }
71 |
72 | const onSignup = (e) => {
73 | setLoading(true);
74 |
75 | const errors = document.getElementById('errors');
76 |
77 | errors.innerHTML = '';
78 |
79 | if (!email) {
80 | errors.innerHTML += 'Email must not be empty';
81 | setLoading(false);
82 | return;
83 | }
84 |
85 | if (!username) {
86 | errors.innerHTML += 'Username must not be empty';
87 | setLoading(false);
88 | return;
89 | }
90 |
91 | if (!password) {
92 | errors.innerHTML += 'Password must not be empty';
93 | setLoading(false);
94 | return;
95 | }
96 |
97 | if (!passwordConfirm) {
98 | errors.innerHTML += 'Confirm Password must not be empty';
99 | setLoading(false);
100 | return;
101 | }
102 |
103 | if (password !== passwordConfirm) {
104 | errors.innerHTML += 'Passwords must match';
105 | setLoading(false);
106 | return;
107 | }
108 |
109 | let locale = navigator.language || navigator.userLanguage;
110 |
111 | if (locale.length > 2) {
112 | locale = locale.substring(0, 2);
113 | }
114 |
115 | window.grecaptcha.ready(() => {
116 | window.grecaptcha.execute(SITE_KEY, { action: 'submit' }).then(async (token) => {
117 | const response = await fetch(`${process.env.API_URL}/v1/user?g_recaptcha_response=${token}`, {
118 | method: 'PUT',
119 | headers: {
120 | 'Accept': 'application/json',
121 | 'Content-Type': 'application/json'
122 | },
123 | body: JSON.stringify({
124 | email: email,
125 | username: username,
126 | password: password,
127 | localization: locale
128 | })
129 | });
130 |
131 | const body = await response.json();
132 |
133 | if (body.status_code === 200) {
134 | setCompleted(true);
135 | setLoading(false);
136 | }
137 | else {
138 | setLoading(false);
139 | errors.innerHTML += body.data;
140 | return;
141 | }
142 | });
143 | });
144 | }
145 |
146 | return (
147 |
148 |
149 | {!completed ? (
150 |
151 |
152 |
189 |
192 | Login
193 |
194 |
195 | ) : (
196 |
197 | Registration complete!
198 | We sent an email to {email}'s inbox for account verification purpose
199 |
200 | )}
201 |
202 |
203 | );
204 | }
--------------------------------------------------------------------------------
/src/pages/signup.module.css:
--------------------------------------------------------------------------------
1 | .signupMain {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | }
6 |
7 | .form {
8 | padding: 24px;
9 | width: 300px;
10 | text-align: center;
11 | }
12 |
13 | form {
14 | margin-top: 22px;
15 | display: flex;
16 | flex-direction: column;
17 | }
18 |
19 | input {
20 | margin: 8px 0;
21 | padding: 12px 18px;
22 | background-color: #151f2e;
23 | border: 1px solid rgba(255, 255, 255, .1);
24 | border-radius: 6px;
25 | color: rgba(255, 255, 255, .6);
26 | outline: none;
27 | transition: .3s;
28 | }
29 |
30 | input:focus, input:active {
31 | border-color: rgba(255, 255, 255, .3);
32 | color: white;
33 | transition: .3s;
34 | }
35 |
36 | button {
37 | margin-top: 8px;
38 | padding: 12px 0;
39 | background-color: #8d46b8;
40 | border: none;
41 | border-radius: 6px;
42 | color: white;
43 | text-transform: uppercase;
44 | font-weight: 600;
45 | transition: .3s;
46 | }
47 |
48 | button:hover, button:active {
49 | cursor: pointer;
50 | transition: .3s;
51 | }
52 |
53 | .errors {
54 | margin: 12px 0;
55 | color: red;
56 | text-align: left;
57 | font-weight: 300;
58 | }
59 |
60 | .login {
61 | display: block;
62 | margin-top: 12px;
63 | text-align: center;
64 | }
--------------------------------------------------------------------------------
/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/.nojekyll
--------------------------------------------------------------------------------
/static/img/aniapi_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/img/aniapi_icon.png
--------------------------------------------------------------------------------
/static/img/aniapi_icon_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/img/aniapi_icon_dark.png
--------------------------------------------------------------------------------
/static/img/aniapi_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/img/aniapi_logo.png
--------------------------------------------------------------------------------
/static/img/anilist_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/img/anilist_logo.png
--------------------------------------------------------------------------------
/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/img/favicon.ico
--------------------------------------------------------------------------------
/static/img/mal_logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AniAPI-Team/AniAPI-Docs/19b04138cf326b9716a80ec1056ca52e14200b48/static/img/mal_logo.jpg
--------------------------------------------------------------------------------