├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── .vscode └── launch.json ├── CNAME ├── README.md ├── babel.config.js ├── docs ├── authentication.mdx ├── errors.mdx ├── intro.mdx ├── oauth │ ├── _category_.json │ ├── authorization_code_grant.mdx │ └── implicit_grant.mdx ├── pagination.mdx ├── rate_limiting.mdx ├── resources │ ├── _category_.json │ ├── anime.mdx │ ├── episode.mdx │ ├── resource.mdx │ ├── song.mdx │ ├── user.mdx │ └── user_story.mdx └── versioning.mdx ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src ├── components │ ├── ApiCodeBlock.js │ ├── ApiCodeBlock.module.css │ ├── ApiTryIt.js │ ├── ApiTryIt.module.css │ ├── Highlight.js │ └── PkceChallenge.js ├── css │ └── custom.css └── pages │ ├── anilist.js │ ├── developer.js │ ├── developer.module.css │ ├── index.js │ ├── index.module.css │ ├── login.js │ ├── login.module.css │ ├── mal.js │ ├── profile.js │ ├── profile.module.css │ ├── signup.js │ └── signup.module.css ├── static ├── .nojekyll └── img │ ├── aniapi_icon.png │ ├── aniapi_icon_dark.png │ ├── aniapi_logo.png │ ├── anilist_logo.png │ ├── favicon.ico │ └── mal_logo.jpg └── yarn.lock /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-latest 16 | steps: 17 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 18 | - name: Check out repo 19 | uses: actions/checkout@v2 20 | 21 | # Node is required for npm 22 | - name: Set up Node 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: "12" 26 | 27 | - name: Create .env file 28 | run: | 29 | cat << EOF >>.env 30 | API_URL=${{ secrets.API_URL }} 31 | ANILIST_CLIENTID=${{ secrets.ANILIST_CLIENTID }} 32 | MAL_CLIENTID=${{ secrets.MAL_CLIENTID }} 33 | MAL_CLIENTSECRET=${{ secrets.MAL_CLIENTSECRET }} 34 | EOF 35 | 36 | # Install and build Docusaurus website 37 | - name: Build Docusaurus website 38 | run: | 39 | npm install 40 | npm run build 41 | 42 | - name: Deploy to GitHub Pages 43 | if: success() 44 | uses: crazy-max/ghaction-github-pages@v2 45 | with: 46 | target_branch: gh-pages 47 | build_dir: build 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | package-lock.json 23 | .env -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Usare IntelliSense per informazioni sui possibili attributi. 3 | // Al passaggio del mouse vengono visualizzate le descrizioni degli attributi esistenti. 4 | // Per altre informazioni, visitare: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Docusaurus Start (Debug)", 9 | "type": "node", 10 | "request": "launch", 11 | "cwd": "${workspaceFolder}", 12 | "program": "${workspaceFolder}/node_modules/@docusaurus/core/bin/docusaurus.js", 13 | "args": [ 14 | "start" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | docs.aniapi.com -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AniAPI Documentation 2 | 3 | This is the official documentation for the AniAPI HTTP Rest APIs, written by the [AniAPI Team](https://github.com/AniAPI-Team) with community contributions. 4 | 5 | ## Contents 6 | 7 | - [Introduction](docs/intro.mdx) 8 | - [Authentication](docs/authentication.mdx) 9 | - [Errors](docs/errors.mdx) 10 | - [Rate Limiting](docs/rate_limiting.mdx) 11 | - [Pagination](docs/pagination.mdx) 12 | - [Versioning](docs/versioning.mdx) 13 | - [Resources](docs/resources) 14 | 15 | ## Contributions 16 | 17 | If you have anything to fix or details to add, first [file an issue](https://github.com/AniAPI-Team/AniAPI-Docs/issues) on GitHub to see if it is likely to be accepted, then file a pull request with your change (one PR per issue). 18 | 19 | This is not intended to be an open wiki; we want to keep it concise and minimal but will accept fixes and suitable additions. 20 | 21 | This documentation is built with [Docusaurus](https://docusaurus.io/docs), please refer to its documentation if you plan to contribute to ours. 22 | 23 | 24 | 25 | 26 | 27 | ## Licence 28 | 29 | Unless otherwise specified, everything in this repository is covered by the following licence: 30 | 31 | [![Creative Commons Attribution-ShareAlike 4.0 International](https://licensebuttons.net/l/by-sa/4.0/88x31.png)](https://creativecommons.org/licenses/by-sa/4.0/) 32 | 33 | ***AniAPI Documentation*** by the [AniAPI Team](https://github.com/AniAPI-Team) is licensed under a [Creative Commons Attribution 4.0 International Licence](https://creativecommons.org/licenses/by-sa/4.0/). 34 | 35 | Based on a work at https://github.com/AniAPI-Team/AniAPI-Docs 36 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/authentication.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Authentication 6 | 7 | import Tabs from '@theme/Tabs'; 8 | import TabItem from '@theme/TabItem'; 9 | 10 | AniAPI uses **[JSON Web Tokens](https://jwt.io/introduction)** to authenticate requests. 11 | Each JWT is referred to an user `id` and is valid for `30` days. 12 | When a JWT expires, the user should login again to obtain a new one. 13 | 14 | You can obtain your JWT on **[your profile page](../profile#jwt)**. 15 | 16 | Authentication on secured endpoints is managed with **HTTP Bearer Auth**, which expects the use of `Authorization` HTTP header: 17 | 18 | 24 | 25 | 26 | ```bash 27 | curl -i https://api.aniapi.com/v1/user_story -H "Authorization: Bearer " 28 | ``` 29 | 30 | 31 | 32 | 33 | ```js 34 | fetch('https://api.aniapi.com/v1/user_story', { 35 | method: 'GET', 36 | headers: { 37 | 'Authorization': 'Bearer ', 38 | 'Content-Type': 'application/json', 39 | 'Accept': 'application/json' 40 | } 41 | }); 42 | ``` 43 | 44 | 45 | 46 | 47 | ## Validation 48 | 49 | Since **JWTs** have an expiration time, you will have to validate your **JWT** before making requests to the **API**. 50 | Therefore you will need to make a validation request to `https://api.aniapi.com/v1/auth/me` in order to find out if your **JWT** is still usable. 51 | 52 | ```bash title="Example request" 53 | curl -i https://api.aniapi.com/v1/auth/me -H "Authorization: Bearer " 54 | ``` 55 | 56 | If your **JWT** is valid you will get a response like below: 57 | 58 | ```js title="Example response" 59 | { 60 | "status_code": 200, 61 | "message": "Hi Dazorn", 62 | "data": { 63 | "username": "Dazorn", 64 | "id": 1, 65 | //... 66 | }, 67 | "version": "1" 68 | } 69 | ``` 70 | 71 | ## OAuth 72 | 73 | AniAPI gives the possibility to access protected endpoints from **third-party apps**. 74 | 75 | You can view and manage all your clients on **[the developer page](../developer)**, after **[signing in](../login)**. 76 | 77 | ### Do I need to use OAuth? 78 | 79 | If you want to allow users to login with their **AniAPI acccount** and have complete access on their data **you will need** to use OAuth. 80 | Otherwise, if your application's scope is **read-only** on public data, you won't need it. 81 | 82 | ### Which grant type should I use? 83 | 84 | It depends on the environment where your app is running: 85 | * use the **[Implicit grant](oauth/implicit_grant)** if your client is a **website** or a **mobile application** 86 | * use the **[Authorization Code grant](oauth/authorization_code_grant)** if you can securely store your **client's credentials** (for example on a **server**) 87 | -------------------------------------------------------------------------------- /docs/errors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Errors 6 | 7 | AniAPI uses conventional **[HTTP response codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)** to indicate the success or failure of an API request. 8 | As a summary: 9 | 10 | * `2xx` range indicates success 11 | * `4xx` range indicates an error regarding the information provided (e.g., a required parameter missing, a resource not found, etc) 12 | * `5xx` range indicates an error with AniAPI's server 13 | 14 | :::caution 15 | 16 | HTTP errors are returned inside the **[HTTP response header](https://developer.mozilla.org/en-US/docs/Glossary/Response_header)**, instead API errors are returned inside the **HTTP response body**! 17 | 18 | ::: 19 | 20 | ## API errors 21 | 22 | When an API request gets an HTTP `200` status code as a response, it could mean that the **API response** contains an error. 23 | These are the possible errors you can have: 24 | 25 | | Status | Reason | 26 | | --- | --- | 27 | | `200 - OK` | Everything worked as expected | 28 | | `400 - Bad request` | The request was unacceptable, often due to missing a required parameter | 29 | | `401 - Unauthorized` | The user is not able to authenticate | 30 | | `403 - Forbidden` | The user doesn't have permissions to perform the request | 31 | | `404 - Not found` | The requested resource doesn't exist | 32 | | `429 - Too many requests` | Too many requests hit the API too quickly from the same origin | 33 | | `500 - Server error` | Something went wrong on AniAPI's end | 34 | 35 | ## Handling errors 36 | 37 | We recommend to focus on HTTP errors just for **[Authentication](authentication)** or **network** problems. 38 | -------------------------------------------------------------------------------- /docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | slug: / 4 | --- 5 | 6 | # Introduction 7 | 8 | import Tabs from '@theme/Tabs'; 9 | import TabItem from '@theme/TabItem'; 10 | 11 | Welcome to AniAPI official documentation. 12 | Our API follows **[REST](https://en.wikipedia.org/wiki/Representational_state_transfer)** architecture, returns **[JSON-Encoded](https://www.json.org/json-en.html)** responses using standard HTTP response codes. 13 | Each response will be formatted like the following: 14 | 15 | ```js title="Example response" 16 | { 17 | "status_code": 200, // The response HTTP status code 18 | "message": "Anime found", // An arbitrary message indicating response content 19 | "data": { // The response JSON-Encoded content 20 | "anilist_id": 1, 21 | "mal_id": 1, 22 | "format": 0, 23 | "status:": 0, 24 | "titles": { 25 | "en": "Cowboy Bebop", 26 | "jp": "カウボーイビバップ" 27 | }, 28 | // ... 29 | "id": 0 30 | }, 31 | "version": "1" // The response API version 32 | } 33 | ``` 34 | 35 | We also support **[OAuth](https://en.wikipedia.org/wiki/OAuth)** as the main authentication system. 36 | You can find more information in **[Authentication](authentication#oauth)**. 37 | 38 | ```http title="BASE URL" 39 | https://api.aniapi.com 40 | ``` 41 | 42 | ## Official client libraries 43 | 44 | 49 |
dotnet add package AniAPI.NET
50 |
-------------------------------------------------------------------------------- /docs/oauth/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "OAuth", 3 | "position": 3 4 | } -------------------------------------------------------------------------------- /docs/oauth/authorization_code_grant.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Authorization Code Grant 6 | 7 | When you have an area to store **your client credentials** safely, you can use the authorization code grant. 8 | 9 | :::caution 10 | 11 | The only one confidential value of your client is the `client_secret`, keep it private! 12 | 13 | ::: 14 | 15 | ## How it works 16 | 17 | 1. The user asks for **Login** inside your application 18 | 2. Instead of building a *login-form*, you just redirect the user to **our OAuth endpoint** 19 | 3. The user authenticates with his credentials 20 | 4. If nothing goes wrong, the AniAPI server redirects the user to your app with an `authorization_code` 21 | 5. Your application uses the code to get the user's `access_token` 22 | 23 | ## Requirements 24 | 25 | * An AniAPI OAuth Client 26 | * A **frontend** application 27 | * A **backend** server 28 | 29 | ## Redirect the user 30 | 31 | Open a browser window (or just redirect if your application is a website) and make a request to `https://api.aniapi.com/v1/oauth`. 32 | The `oauth` endpoint expects some parameters to identify the client calling it: 33 | 34 | | Name | Required | Description | 35 | | --- | --- | --- | 36 | | `client_id` | Yes | Your client ID | 37 | | `redirect_uri` | Yes | Your client redirect URI | 38 | | `response_type` | Yes | For authorization code grant pass `code` | 39 | | `state` | No | A random string generated by your application | 40 | 41 | :::info 42 | 43 | The `client_id` and `redirect_uri` values must match your client's one. 44 | 45 | ::: 46 | 47 | The `state` parameter (optional) is used to protect your application from *cross-site request forgery* (**CSRF**). 48 | If provided, the AniAPI server will return it alongside the user's `access_token`. 49 | Verify it against the value you provided before to validate the response. 50 | 51 | ```http title="Example request URL" 52 | https://api.aniapi.com/v1/oauth? 53 | response_type=code 54 | &client_id= 55 | &redirect_uri= 56 | &state= 57 | ``` 58 | 59 | ## Retrieve the authorization code 60 | 61 | Once the user approved the **Authentication** request and completed the login step, the **AniAPI server** will redirect them back to your application. 62 | Let's assume you provided `http://localhost:3000/auth` as `redirect_uri` value. This will be the redirection URL: 63 | 64 | ```http title="Example redirect URL" 65 | http://localhost:3000/auth/?code=&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 |
308 |

Name

309 | setName(e.target.value)} 313 | value={name} /> 314 |

Redirect URI

315 | setRedirectUri(e.target.value)} 319 | value={redirectUri} /> 320 |
322 | {loading ? ( 323 | 324 | ) : ( 325 | 328 | )} 329 |
330 |
331 |
333 |

Edit

334 |
335 |

Name

336 | setName(e.target.value)} 340 | value={name} /> 341 |

Redirect URI

342 | setRedirectUri(e.target.value)} 346 | value={redirectUri} /> 347 |
349 | {loading ? ( 350 | 351 | ) : ( 352 | 355 | )} 356 |
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 | 170 | 171 | 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 |
123 | setEmail(e.target.value)} 127 | onKeyPress={onEmailKeyPress} 128 | value={email} /> 129 | setPassword(e.target.value)} 134 | onKeyPress={onPasswordKeyPress} 135 | value={password} /> 136 |
138 | {loading ? ( 139 | 140 | ) : ( 141 | 144 | )} 145 |
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 |
252 |

Password

253 | setPassword(e.target.value)} 258 | value={password} /> 259 |

Confirm Password

260 | setPasswordConfirm(e.target.value)} 265 | value={passwordConfirm} /> 266 |

Locale

267 | 272 |

Gender

273 |
274 | 275 | setGender(0)} /> 281 | 282 | setGender(1)} /> 288 | 289 | setGender(2)} /> 295 |
296 |

Avatar

297 |
298 | 299 | setAvatar("none")} /> 305 | 306 | setAvatar("anilist")} /> 312 | 313 | setAvatar("mal")} /> 319 |
320 |
322 | {loading ? ( 323 | 324 | ) : ( 325 | 328 | )} 329 |
331 |
332 |
333 |
335 |

Trackers

336 | 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 |
153 | setEmail(e.target.value)} 157 | onKeyPress={onEmailKeyPress} 158 | value={email} /> 159 | setUsername(e.target.value)} 163 | onKeyPress={onUsernameKeyPress} 164 | value={username} /> 165 | setPassword(e.target.value)} 170 | onKeyPress={onPasswordKeyPress} 171 | value={password} /> 172 | setPasswordConfirm(e.target.value)} 177 | onKeyPress={onPasswordConfirmKeyPress} 178 | value={passwordConfirm} /> 179 |
181 | {loading ? ( 182 | 183 | ) : ( 184 | 187 | )} 188 |
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 --------------------------------------------------------------------------------