Merge pull request #122 from ghoshRitesh12/feature/proper-envs
feature: add proper envs and logging support
This commit is contained in:
@@ -23,4 +23,7 @@ jobs:
|
|||||||
run: npm i
|
run: npm i
|
||||||
|
|
||||||
- name: Check lint
|
- name: Check lint
|
||||||
run: npm run lint:ci
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Check prettier
|
||||||
|
run: npm run format:check
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Skip Husky install in production and CI
|
||||||
|
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const husky = (await import("husky")).default;
|
||||||
|
console.log(husky());
|
||||||
+3
-17
@@ -1,35 +1,21 @@
|
|||||||
# [2.12.0](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.3...v2.12.0) (2025-04-14)
|
# [2.12.0](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.3...v2.12.0) (2025-04-14)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* **getNextEpisodeSchedule:** add `/anime/:animeId/next-episode-schedule` endpoint ([f5cd341](https://github.com/ghoshRitesh12/aniwatch-api/commit/f5cd3415d8134da1ab1e3b3f8f9be6b5212aa353))
|
- **getNextEpisodeSchedule:** add `/anime/:animeId/next-episode-schedule` endpoint ([f5cd341](https://github.com/ghoshRitesh12/aniwatch-api/commit/f5cd3415d8134da1ab1e3b3f8f9be6b5212aa353))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.11.3](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.2...v2.11.3) (2025-03-20)
|
## [2.11.3](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.2...v2.11.3) (2025-03-20)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.11.2](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.1...v2.11.2) (2025-03-15)
|
## [2.11.2](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.1...v2.11.2) (2025-03-15)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.11.1](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.0...v2.11.1) (2025-01-01)
|
## [2.11.1](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.11.0...v2.11.1) (2025-01-01)
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
* **ts build error:** fixed ts build error due to conflicting types ([cb5a467](https://github.com/ghoshRitesh12/aniwatch-api/commit/cb5a4672a8c3b0729bbb4522a3af252f7b336b97))
|
- **ts build error:** fixed ts build error due to conflicting types ([cb5a467](https://github.com/ghoshRitesh12/aniwatch-api/commit/cb5a4672a8c3b0729bbb4522a3af252f7b336b97))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# [2.11.0](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.10.0...v2.11.0) (2024-12-25)
|
# [2.11.0](https://github.com/ghoshRitesh12/aniwatch-api/compare/v2.10.0...v2.11.0) (2024-12-25)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* **`/qtip`:** add new `/qtip` endpoint ([f0acd89](https://github.com/ghoshRitesh12/aniwatch-api/commit/f0acd89d87e5e62c12e20a95225ca9261fefe411))
|
- **`/qtip`:** add new `/qtip` endpoint ([f0acd89](https://github.com/ghoshRitesh12/aniwatch-api/commit/f0acd89d87e5e62c12e20a95225ca9261fefe411))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+17
-17
@@ -8,8 +8,8 @@ Thank you for your interest in contributing to aniwatch-api. We appreciate whate
|
|||||||
- [Ground Rules & Expectations](#ground-rules--expectations)
|
- [Ground Rules & Expectations](#ground-rules--expectations)
|
||||||
- [How To Contribute](#how-to-contribute)
|
- [How To Contribute](#how-to-contribute)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Clone the repository](#clone-the-repository)
|
- [Clone the repository](#clone-the-repository)
|
||||||
- [Project Structure](#project-structure)
|
- [Project Structure](#project-structure)
|
||||||
- [Commit Messages](#commit-messages)
|
- [Commit Messages](#commit-messages)
|
||||||
|
|
||||||
## Types of contributions we are looking for
|
## Types of contributions we are looking for
|
||||||
@@ -42,10 +42,10 @@ To contribute to this project, you must know the following:
|
|||||||
- [NodeJS](https://nodejs.org/)
|
- [NodeJS](https://nodejs.org/)
|
||||||
- [TypeScript](https://www.typescriptlang.org/)
|
- [TypeScript](https://www.typescriptlang.org/)
|
||||||
- Web Scraping
|
- Web Scraping
|
||||||
- [Cheerio](https://cheerio.js.org/)
|
- [Cheerio](https://cheerio.js.org/)
|
||||||
- [Axios](https://axios-http.com/docs/intro)
|
- [Axios](https://axios-http.com/docs/intro)
|
||||||
- [CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
|
- [CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
|
||||||
- [Browser Dev Tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools)
|
- [Browser Dev Tools](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/Tools_and_setup/What_are_browser_developer_tools)
|
||||||
|
|
||||||
### Clone the repository
|
### Clone the repository
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ git clone https://github.com/<your_username>/aniwatch-api
|
|||||||
|
|
||||||
3. Creating a new branch <br/>
|
3. Creating a new branch <br/>
|
||||||
Replace \<new-branch-name> with any of the following naming conventions: <br/>
|
Replace \<new-branch-name> with any of the following naming conventions: <br/>
|
||||||
- `feature/<branch-name>` - for adding new features
|
- `feature/<branch-name>` - for adding new features
|
||||||
- `bug/<branch-name>` - for fixing known bugs
|
- `bug/<branch-name>` - for fixing known bugs
|
||||||
- `misc/<branch-name>` - for anything other than bug or features
|
- `misc/<branch-name>` - for anything other than bug or features
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git checkout -b <new-branch-name>
|
git checkout -b <new-branch-name>
|
||||||
@@ -70,14 +70,14 @@ git checkout -b <new-branch-name>
|
|||||||
|
|
||||||
- `src` directory contains all the source code required for this project
|
- `src` directory contains all the source code required for this project
|
||||||
|
|
||||||
- `controllers` directory contains all the controller logic
|
- `controllers` directory contains all the controller logic
|
||||||
- `types` directory contains all types & interfaces used for this project
|
- `types` directory contains all types & interfaces used for this project
|
||||||
- `parsers` directory contains all the parsing aka scraping logic
|
- `parsers` directory contains all the parsing aka scraping logic
|
||||||
- `routes` directory contains all the routers
|
- `routes` directory contains all the routers
|
||||||
- `utils` directory contains handy utility methods and properties
|
- `utils` directory contains handy utility methods and properties
|
||||||
- `config` directory contains api configuration related files
|
- `config` directory contains api configuration related files
|
||||||
- `extractors` directory contains anime streaming url extractor files
|
- `extractors` directory contains anime streaming url extractor files
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
|
|
||||||
- `test` directory contains all the tests that needs to be evaluated
|
- `test` directory contains all the tests that needs to be evaluated
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# <p align="center">Aniwatch API</p>
|
# <p align="center">Aniwatch API</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
A free RESTful API serving anime information from <a href="https://hianime.to" target="_blank">hianime.to</a>
|
A free RESTful API serving anime information from <a href="https://hianimez.to" target="_blank">hianimez.to</a>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
>
|
>
|
||||||
> 1. [https://api-aniwatch.onrender.com](https://api-aniwatch.onrender.com/) is only meant to demo the API and has rate-limiting enabled to minimize bandwidth consumption. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be.
|
> 1. [https://api-aniwatch.onrender.com](https://api-aniwatch.onrender.com/) is only meant to demo the API and has rate-limiting enabled to minimize bandwidth consumption. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be.
|
||||||
> 2. This API is just an unofficial API for [hianime.to](https://hianime.to) and is in no other way officially related to the same.
|
> 2. This API is just an unofficial API for [hianimez.to](https://hianimez.to) and is in no other way officially related to the same.
|
||||||
> 3. The content that this API provides is not mine, nor is it hosted by me. These belong to their respective owners. This API just demonstrates how to build an API that scrapes websites and uses their content.
|
> 3. The content that this API provides is not mine, nor is it hosted by me. These belong to their respective owners. This API just demonstrates how to build an API that scrapes websites and uses their content.
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
// npx vitest run animeAZList.test.ts
|
||||||
|
test("returns az list anime", async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getAZList("0-9", 1);
|
||||||
|
|
||||||
|
expect(data.animes).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
|
const animeId = "steinsgate-3";
|
||||||
|
|
||||||
|
// npx vitest run animeAboutInfo.test.ts
|
||||||
|
test(`GET /api/v2/hianime/anime/${animeId}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getInfo(animeId);
|
||||||
|
|
||||||
|
expect(data.anime.info.name).not.toEqual(null);
|
||||||
|
expect(data.recommendedAnimes).not.toEqual([]);
|
||||||
|
expect(data.mostPopularAnimes).not.toEqual([]);
|
||||||
|
expect(Object.keys(data.anime.moreInfo)).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
const category = "subbed-anime";
|
||||||
|
|
||||||
|
// npx vitest run animeCategory.test.ts
|
||||||
|
test(`GET /api/v2/hianime/category/${category}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getCategoryAnime(category);
|
||||||
|
|
||||||
|
expect(data.animes).not.toEqual([]);
|
||||||
|
expect(data.genres).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.today).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.week).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.month).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -7,12 +7,12 @@ const category = "sub";
|
|||||||
|
|
||||||
// npx vitest run animeEpisodeSrcs.test.ts
|
// npx vitest run animeEpisodeSrcs.test.ts
|
||||||
test(`GET /api/v2/hianime/episode/sources?animeEpisodeId=${animeEpisodeId}&server=${server}&category=${category}`, async () => {
|
test(`GET /api/v2/hianime/episode/sources?animeEpisodeId=${animeEpisodeId}&server=${server}&category=${category}`, async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
const data = await hianime.getEpisodeSources(
|
const data = await hianime.getEpisodeSources(
|
||||||
animeEpisodeId,
|
animeEpisodeId,
|
||||||
server,
|
server,
|
||||||
category
|
category
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(data.sources).not.toEqual([]);
|
expect(data.sources).not.toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -5,9 +5,9 @@ const animeId = "steinsgate-3";
|
|||||||
|
|
||||||
// npx vitest run animeEpisodes.test.ts
|
// npx vitest run animeEpisodes.test.ts
|
||||||
test(`GET /api/v2/hianime/anime/${animeId}/episodes`, async () => {
|
test(`GET /api/v2/hianime/anime/${animeId}/episodes`, async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
const data = await hianime.getEpisodes(animeId);
|
const data = await hianime.getEpisodes(animeId);
|
||||||
|
|
||||||
expect(data.totalEpisodes).not.toEqual(0);
|
expect(data.totalEpisodes).not.toEqual(0);
|
||||||
expect(data.episodes).not.toEqual([]);
|
expect(data.episodes).not.toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -6,11 +6,11 @@ const page = 2;
|
|||||||
|
|
||||||
// npx vitest run animeGenre.test.ts
|
// npx vitest run animeGenre.test.ts
|
||||||
test(`GET /api/v2/hianime/genre/${genreName}?page=${page}`, async () => {
|
test(`GET /api/v2/hianime/genre/${genreName}?page=${page}`, async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
|
|
||||||
const data = await hianime.getGenreAnime(genreName, page);
|
const data = await hianime.getGenreAnime(genreName, page);
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
expect(data.animes).not.toEqual([]);
|
||||||
expect(data.genres).not.toEqual([]);
|
expect(data.genres).not.toEqual([]);
|
||||||
expect(data.topAiringAnimes).not.toEqual([]);
|
expect(data.topAiringAnimes).not.toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
const producerName = "toei-animation";
|
||||||
|
const page = 2;
|
||||||
|
|
||||||
|
// npx vitest run animeProducer.test.ts
|
||||||
|
test(`GET /api/v2/hianime/producer/${producerName}?page=${page}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getProducerAnimes(producerName, page);
|
||||||
|
|
||||||
|
expect(data.animes).not.toEqual([]);
|
||||||
|
expect(data.topAiringAnimes).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.today).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.week).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.month).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
const animeId = "one-piece-100";
|
||||||
|
|
||||||
|
// npx vitest run animeQtip.test.ts
|
||||||
|
test(`returns ${animeId} anime qtip info`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getQtipInfo(animeId);
|
||||||
|
|
||||||
|
expect(data.anime.id).not.toEqual(null);
|
||||||
|
expect(data.anime.name).not.toEqual(null);
|
||||||
|
expect(data.anime.description).not.toEqual(null);
|
||||||
|
expect(data.anime.genres).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -4,14 +4,14 @@ import { HiAnime } from "aniwatch";
|
|||||||
const query = "monster";
|
const query = "monster";
|
||||||
const page = 1;
|
const page = 1;
|
||||||
const filter: HiAnime.SearchFilters = {
|
const filter: HiAnime.SearchFilters = {
|
||||||
genres: "seinen,psychological",
|
genres: "seinen,psychological",
|
||||||
};
|
};
|
||||||
|
|
||||||
// npx vitest run animeSearch.test.ts
|
// npx vitest run animeSearch.test.ts
|
||||||
test(`GET /api/v2/hianime/search?q=${query}&page=${page}&genres=${filter.genres}`, async () => {
|
test(`GET /api/v2/hianime/search?q=${query}&page=${page}&genres=${filter.genres}`, async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
const data = await hianime.search(query, page, filter);
|
const data = await hianime.search(query, page, filter);
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
expect(data.animes).not.toEqual([]);
|
||||||
expect(data.mostPopularAnimes).not.toEqual([]);
|
expect(data.mostPopularAnimes).not.toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -5,8 +5,8 @@ const query = "one piece";
|
|||||||
|
|
||||||
// npx vitest run animeSearchSuggestion.test.ts
|
// npx vitest run animeSearchSuggestion.test.ts
|
||||||
test(`GET /api/v2/hianime/search/suggestion?q=${query}`, async () => {
|
test(`GET /api/v2/hianime/search/suggestion?q=${query}`, async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
const data = await hianime.searchSuggestions(query);
|
const data = await hianime.searchSuggestions(query);
|
||||||
|
|
||||||
expect(data.suggestions).not.toEqual([]);
|
expect(data.suggestions).not.toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
const animeEpisodeId = "steinsgate-0-92?ep=2055";
|
||||||
|
|
||||||
|
// npx vitest run episodeServers.test.ts
|
||||||
|
test(`GET /api/v2/hianime/episode/servers?animeEpisodeId=${animeEpisodeId}`, async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getEpisodeServers(animeEpisodeId);
|
||||||
|
|
||||||
|
expect(data.episodeId).not.toEqual(null);
|
||||||
|
expect(data.episodeNo).not.toEqual(0);
|
||||||
|
expect(data.sub).not.toEqual([]);
|
||||||
|
expect(data.dub).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -5,13 +5,13 @@ const padZero = (num: number) => (num < 10 ? `0${num}` : num.toString());
|
|||||||
|
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
const date = `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(
|
const date = `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(
|
||||||
d.getDate()
|
d.getDate()
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
// npx vitest run estimatedSchedule.test.ts
|
// npx vitest run estimatedSchedule.test.ts
|
||||||
test(`GET /api/v2/hianime/schedule?date=${date}`, async () => {
|
test(`GET /api/v2/hianime/schedule?date=${date}`, async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
const data = await hianime.getEstimatedSchedule(date);
|
const data = await hianime.getEstimatedSchedule(date);
|
||||||
|
|
||||||
expect(data.scheduledAnimes).not.toEqual([]);
|
expect(data.scheduledAnimes).not.toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
test("GET /api/v2/hianime/home", async () => {
|
||||||
|
const hianime = new HiAnime.Scraper();
|
||||||
|
const data = await hianime.getHomePage();
|
||||||
|
|
||||||
|
expect(data.spotlightAnimes).not.toEqual([]);
|
||||||
|
expect(data.trendingAnimes).not.toEqual([]);
|
||||||
|
expect(data.latestEpisodeAnimes).not.toEqual([]);
|
||||||
|
expect(data.topUpcomingAnimes).not.toEqual([]);
|
||||||
|
expect(data.topAiringAnimes).not.toEqual([]);
|
||||||
|
expect(data.mostPopularAnimes).not.toEqual([]);
|
||||||
|
expect(data.mostFavoriteAnimes).not.toEqual([]);
|
||||||
|
expect(data.latestCompletedAnimes).not.toEqual([]);
|
||||||
|
expect(data.genres).not.toEqual([]);
|
||||||
|
|
||||||
|
expect(data.top10Animes.today).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.week).not.toEqual([]);
|
||||||
|
expect(data.top10Animes.month).not.toEqual([]);
|
||||||
|
});
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import { HiAnime } from "aniwatch";
|
import { HiAnime } from "aniwatch";
|
||||||
|
|
||||||
|
function padZero(num: number) {
|
||||||
|
return num < 10 ? `0${num}` : num.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// npx vitest run nextEpisodeSchedule.test.ts
|
// npx vitest run nextEpisodeSchedule.test.ts
|
||||||
test("returns anime next episode schedule", async () => {
|
test("returns anime next episode schedule", async () => {
|
||||||
const hianime = new HiAnime.Scraper();
|
const hianime = new HiAnime.Scraper();
|
||||||
|
|
||||||
const animeId = "one-piece-100";
|
const d = new Date();
|
||||||
|
const scheduleData = await hianime.getEstimatedSchedule(
|
||||||
|
`${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const animeId = scheduleData.scheduledAnimes[0].id!;
|
||||||
const data = await hianime.getNextEpisodeSchedule(animeId);
|
const data = await hianime.getNextEpisodeSchedule(animeId);
|
||||||
|
|
||||||
expect(data.airingISOTimestamp).not.toEqual(null);
|
expect(data.airingISOTimestamp).not.toEqual(null);
|
||||||
+55
-49
@@ -1,51 +1,57 @@
|
|||||||
{
|
{
|
||||||
"name": "aniwatch-api",
|
"name": "aniwatch-api",
|
||||||
"version": "2.12.0",
|
"version": "2.12.0",
|
||||||
"description": "Node.js API for obtaining anime information from hianime.to",
|
"description": "Node.js API for obtaining anime information from hianimez.to",
|
||||||
"main": "src/server.ts",
|
"main": "src/server.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsx src/server.ts",
|
"start": "tsx src/server.ts",
|
||||||
"dev": "tsx watch src/server.ts",
|
"dev": "tsx watch src/server.ts",
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"vercel-build": "echo \"Hello\"",
|
"vercel-build": "echo \"Hello\"",
|
||||||
"prepare": "husky install",
|
"prepare": "node .husky/install.mjs",
|
||||||
"test": "vitest run --config vitest.config.ts",
|
"test": "vitest run --config vitest.config.ts",
|
||||||
"healthcheck": "curl -f http://localhost:4000/health",
|
"healthcheck": "curl -f http://localhost:4000/health",
|
||||||
"lint": "prettier --cache --write .",
|
"lint": "tsc",
|
||||||
"lint:ci": "prettier --cache --check ."
|
"format": "prettier --cache --write .",
|
||||||
},
|
"format:check": "prettier --cache --check ."
|
||||||
"repository": {
|
},
|
||||||
"type": "git",
|
"repository": {
|
||||||
"url": "git+https://github.com/ghoshRitesh12/aniwatch-api.git"
|
"type": "git",
|
||||||
},
|
"url": "git+https://github.com/ghoshRitesh12/aniwatch-api.git"
|
||||||
"bugs": {
|
},
|
||||||
"url": "https://github.com/ghoshRitesh12/aniwatch-api/issues"
|
"bugs": {
|
||||||
},
|
"url": "https://github.com/ghoshRitesh12/aniwatch-api/issues"
|
||||||
"homepage": "https://github.com/ghoshRitesh12/aniwatch-api#readme",
|
},
|
||||||
"keywords": [
|
"homepage": "https://github.com/ghoshRitesh12/aniwatch-api#readme",
|
||||||
"anime",
|
"keywords": [
|
||||||
"weeb",
|
"anime",
|
||||||
"hianime",
|
"weeb",
|
||||||
"aniwatch",
|
"scraper",
|
||||||
"scraper"
|
"zoro.to",
|
||||||
],
|
"aniwatch.to",
|
||||||
"author": "https://github.com/ghoshRitesh12",
|
"hianime.to",
|
||||||
"license": "MIT",
|
"hianimez.to"
|
||||||
"dependencies": {
|
],
|
||||||
"@hono/node-server": "^1.13.7",
|
"author": "https://github.com/ghoshRitesh12",
|
||||||
"aniwatch": "^2.18.3",
|
"license": "MIT",
|
||||||
"dotenv": "^16.4.7",
|
"dependencies": {
|
||||||
"hono": "^4.6.15",
|
"@hono/node-server": "^1.14.1",
|
||||||
"hono-rate-limiter": "^0.4.2",
|
"aniwatch": "^2.22.0",
|
||||||
"ioredis": "^5.4.2"
|
"dotenv": "^16.5.0",
|
||||||
},
|
"envalid": "^8.0.0",
|
||||||
"devDependencies": {
|
"hono": "^4.7.9",
|
||||||
"@types/node": "^22.10.3",
|
"hono-rate-limiter": "^0.4.2",
|
||||||
"husky": "^9.1.7",
|
"ioredis": "^5.6.1",
|
||||||
"prettier": "^3.4.2",
|
"pino": "^9.6.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.4"
|
||||||
"typescript": "^5.7.2",
|
},
|
||||||
"vitest": "^2.1.8"
|
"devDependencies": {
|
||||||
}
|
"@types/node": "^22.15.17",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"pino-pretty": "^13.0.0",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vitest": "^3.1.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+226
-225
@@ -2,276 +2,277 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Aniwatch API</title>
|
<title>Aniwatch API</title>
|
||||||
<meta name="content-language" content="en">
|
<meta name="content-language" content="en">
|
||||||
<!-- <meta http-equiv="Content-Security-Policy"
|
<!-- <meta http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self' https://api-aniwatch.onrender.com; style-src 'unsafe-inline'; img-src 'self' https://raw.githubusercontent.com https://img.shields.io https://contrib.rocks"> -->
|
content="default-src 'self' https://api-aniwatch.onrender.com; style-src 'unsafe-inline'; img-src 'self' https://raw.githubusercontent.com https://img.shields.io https://contrib.rocks"> -->
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="title" content="Aniwatch API">
|
<meta name="title" content="Aniwatch API">
|
||||||
<meta name="theme-color" content="#d5b3ff">
|
<meta name="theme-color" content="#d5b3ff">
|
||||||
<meta name="color-scheme" content="dark">
|
<meta name="color-scheme" content="dark">
|
||||||
<link rel="canonical" href="https://api-aniwatch.onrender.com">
|
<link rel="canonical" href="https://api-aniwatch.onrender.com">
|
||||||
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1">
|
||||||
<meta property="og:image"
|
<meta property="og:image"
|
||||||
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
||||||
<meta property="og:url" content="https://api-aniwatch.onrender.com">
|
<meta property="og:url" content="https://api-aniwatch.onrender.com">
|
||||||
<meta property="og:image:width" content="500">
|
<meta property="og:image:width" content="500">
|
||||||
<meta property="og:image:height" content="500">
|
<meta property="og:image:height" content="500">
|
||||||
<meta property="og:site_name" content="aniwatch">
|
<meta property="og:site_name" content="aniwatch">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta name="twitter:card" content="summary_image">
|
<meta name="twitter:card" content="summary_image">
|
||||||
<meta name="twitter:site" content="@aniwatch-api">
|
<meta name="twitter:site" content="@aniwatch-api">
|
||||||
<meta name="twitter:title" content="Aniwatch API">
|
<meta name="twitter:title" content="Aniwatch API">
|
||||||
<meta name="twitter:description" content="Node.js API for obtaining anime information from hianime.to">
|
<meta name="twitter:description" content="Node.js API for obtaining anime information from hianimez.to">
|
||||||
<meta name="twitter:image:src"
|
<meta name="twitter:image:src"
|
||||||
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
||||||
<meta name="keywords" content="hianime api scraper anime aniwatch node express typescript">
|
<meta name="keywords" content="hianime api scraper anime aniwatch node express typescript">
|
||||||
<meta property="og:title" content="Aniwatch API">
|
<meta property="og:title" content="Aniwatch API">
|
||||||
<meta name="description" content="Node.js API for obtaining anime information from hianime.to">
|
<meta name="description" content="Node.js API for obtaining anime information from hianimez.to">
|
||||||
<meta property="og:description" content="Node.js API for obtaining anime information from hianime.to">
|
<meta property="og:description" content="Node.js API for obtaining anime information from hianimez.to">
|
||||||
<link rel="shortcut icon"
|
<link rel="shortcut icon"
|
||||||
href="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
href="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
--accent: #d5b3ff;
|
box-sizing: border-box;
|
||||||
box-sizing: border-box;
|
font-family: sans-serif;
|
||||||
font-family: sans-serif;
|
transition:
|
||||||
transition:
|
0.15s ease scale,
|
||||||
0.15s ease scale,
|
0.15s ease width,
|
||||||
0.15s ease width,
|
0.15s ease height,
|
||||||
0.15s ease height,
|
0.15s ease gap;
|
||||||
0.15s ease gap;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
color-scheme: dark;
|
--accent: #d5b3ff;
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
height: 100%;
|
||||||
display: flex;
|
color-scheme: dark;
|
||||||
flex-direction: column;
|
}
|
||||||
font-family: sans-serif;
|
|
||||||
background-color: #151518;
|
|
||||||
padding: 0rem 1rem 0 1rem;
|
|
||||||
min-height: 98vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
body {
|
||||||
margin-block: auto;
|
display: flex;
|
||||||
display: flex;
|
flex-direction: column;
|
||||||
flex-direction: column;
|
font-family: sans-serif;
|
||||||
align-items: center;
|
background-color: #151518;
|
||||||
justify-content: start;
|
padding: 0rem 1rem 0 1rem;
|
||||||
position: relative;
|
min-height: 98vh;
|
||||||
isolation: isolate;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
section::before {
|
section {
|
||||||
content: "";
|
margin-block: auto;
|
||||||
width: 100%;
|
display: flex;
|
||||||
height: 100%;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
position: absolute;
|
section::before {
|
||||||
inset: -2rem 0 0 0;
|
content: "";
|
||||||
isolation: isolate;
|
width: 100%;
|
||||||
z-index: -1;
|
height: 100%;
|
||||||
|
|
||||||
background-color: #7300ff;
|
position: absolute;
|
||||||
opacity: .35;
|
inset: -2rem 0 0 0;
|
||||||
border-radius: 50%;
|
isolation: isolate;
|
||||||
filter: blur(12rem);
|
z-index: -1;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
background-color: #7300ff;
|
||||||
section::before {
|
opacity: .35;
|
||||||
background-color: #7300ffbb;
|
border-radius: 50%;
|
||||||
opacity: .3;
|
filter: blur(12rem);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
@media (min-width: 640px) {
|
||||||
position: fixed;
|
section::before {
|
||||||
top: 0;
|
background-color: #7300ffbb;
|
||||||
bottom: 0;
|
opacity: .3;
|
||||||
left: 0;
|
}
|
||||||
right: 0;
|
}
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
width: 100%;
|
.overlay {
|
||||||
min-height: 100%;
|
position: fixed;
|
||||||
isolation: isolate;
|
top: 0;
|
||||||
z-index: -1;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
}
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
isolation: isolate;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
.overlay .child {
|
}
|
||||||
background-position: center center;
|
|
||||||
background-size: cover;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
|
|
||||||
opacity: .4;
|
.overlay .child {
|
||||||
filter: blur(5rem);
|
background-position: center center;
|
||||||
background-image: url(https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/wano_kuni.png);
|
background-size: cover;
|
||||||
}
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
opacity: .4;
|
||||||
.overlay .child {
|
filter: blur(5rem);
|
||||||
opacity: .35;
|
background-image: url(https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/wano_kuni.png);
|
||||||
filter: blur(8rem);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay::before {
|
@media (min-width: 640px) {
|
||||||
content: "";
|
.overlay .child {
|
||||||
position: absolute;
|
opacity: .35;
|
||||||
top: 0;
|
filter: blur(8rem);
|
||||||
bottom: 0;
|
}
|
||||||
left: 0;
|
}
|
||||||
right: 0;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
background: url("https://hianime.to/images/live-thumb.png") repeat;
|
.overlay::before {
|
||||||
}
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
h2 {
|
background: url("https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/dot.png") repeat;
|
||||||
color: var(--accent);
|
}
|
||||||
margin-top: 0;
|
|
||||||
font-size: 1.65rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-align: center;
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
h2 {
|
||||||
line-height: 1.3;
|
color: var(--accent);
|
||||||
text-wrap: balance;
|
margin-top: 0;
|
||||||
color: var(--accent);
|
font-size: 1.65rem;
|
||||||
}
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
a {
|
||||||
object-fit: cover;
|
line-height: 1.3;
|
||||||
max-width: 100%;
|
text-wrap: balance;
|
||||||
font-size: .9rem;
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#anime_girl {
|
img {
|
||||||
width: 100%;
|
object-fit: cover;
|
||||||
max-width: 10rem;
|
max-width: 100%;
|
||||||
}
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
#anime_girl {
|
||||||
#anime_girl {
|
width: 100%;
|
||||||
max-width: 12rem;
|
max-width: 10rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.img-wrapper {
|
@media (min-width: 640px) {
|
||||||
aspect-ratio: 1/1;
|
#anime_girl {
|
||||||
max-width: 12rem;
|
max-width: 12rem;
|
||||||
border-radius: 1rem;
|
}
|
||||||
overflow: hidden;
|
}
|
||||||
width: fit-content;
|
|
||||||
margin: 1rem auto 1.5rem auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-stats-container {
|
.img-wrapper {
|
||||||
display: flex;
|
aspect-ratio: 1/1;
|
||||||
gap: .2rem .5rem;
|
max-width: 12rem;
|
||||||
align-items: center;
|
border-radius: 1rem;
|
||||||
justify-content: center;
|
overflow: hidden;
|
||||||
flex-wrap: wrap;
|
width: fit-content;
|
||||||
margin-bottom: 1rem;
|
margin: 1rem auto 1.5rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.api-stats-container a {
|
.api-stats-container {
|
||||||
flex-shrink: 0;
|
display: flex;
|
||||||
user-select: none;
|
gap: .2rem .5rem;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.contributors {
|
.api-stats-container a {
|
||||||
margin-top: 1rem;
|
flex-shrink: 0;
|
||||||
text-align: center;
|
user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.contributors {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
IMPORTANT:
|
IMPORTANT:
|
||||||
1. The hosted version of this API is only meant to demo the API and would have rate-limiting enabled to minimise bandwidth
|
1. The hosted version of this API is only meant to demo the API and would have rate-limiting enabled to minimise bandwidth
|
||||||
consumption. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be.
|
consumption. It is recommended to deploy your own instance for personal use by customizing the API as you need it to be.
|
||||||
2. This API is just an unofficial API for hianime.to and is in no other way officially related to the same.
|
2. This API is just an unofficial API for hianimez.to and is in no other way officially related to the same.
|
||||||
3. The content that this API provides is not mine, nor is it hosted by me. These belong to their respective owners. This
|
3. The content that this API provides is not mine, nor is it hosted by me. These belong to their respective owners. This
|
||||||
API just demonstrates how to build an API that scrapes websites and uses their content.
|
API just demonstrates how to build an API that scrapes websites and uses their content.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<span class="overlay">
|
<span class="overlay">
|
||||||
<span class="child"></span>
|
<span class="child"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>
|
<h2>
|
||||||
Welcome to the unofficial
|
Welcome to the unofficial
|
||||||
<a href="https://hianime.to/home" style="text-underline-offset: 3px;">hianime.to</a>
|
<a href="https://hianimez.to/home" style="text-underline-offset: 3px;">hianimez.to</a>
|
||||||
API
|
API
|
||||||
<span style="-webkit-text-fill-color: var(--accent)">⚔️</span>
|
<span style="-webkit-text-fill-color: var(--accent)">⚔️</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="api-stats-container">
|
<div class="api-stats-container">
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/stargazers">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/stargazers">
|
||||||
<img src="https://img.shields.io/github/stars/ghoshRitesh12/aniwatch-api?style=social" alt="Stars"
|
<img src="https://img.shields.io/github/stars/ghoshRitesh12/aniwatch-api?style=social" alt="Stars"
|
||||||
decoding="async">
|
decoding="async">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/network/members">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/network/members">
|
||||||
<img src="https://img.shields.io/github/forks/ghoshRitesh12/aniwatch-api?style=social" alt="Forks"
|
<img src="https://img.shields.io/github/forks/ghoshRitesh12/aniwatch-api?style=social" alt="Forks"
|
||||||
decoding="async">
|
decoding="async">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/issues?q=is%3Aissue+is%3Aopen+">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/issues?q=is%3Aissue+is%3Aopen+">
|
||||||
<img src="https://img.shields.io/github/issues/ghoshRitesh12/aniwatch-api?style=social&logo=github" alt="Issues"
|
<img src="https://img.shields.io/github/issues/ghoshRitesh12/aniwatch-api?style=social&logo=github"
|
||||||
decoding="async">
|
alt="Issues" decoding="async">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest">
|
||||||
<img
|
<img src="https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch-api?display_name=release&style=social&logo=github"
|
||||||
src="https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch-api?display_name=release&style=social&logo=github"
|
alt="Version" decoding="async">
|
||||||
alt="Version" decoding="async">
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
<div style="text-align: center;">
|
||||||
<div class="img-wrapper">
|
<div class="img-wrapper">
|
||||||
<img draggable="false" id="anime_girl"
|
<img draggable="false" id="anime_girl"
|
||||||
src="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png"
|
src="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png"
|
||||||
alt="kawai_anime_girl" decoding="async" fetchpriority="high" style="user-select: none;" />
|
alt="kawai_anime_girl" decoding="async" fetchpriority="high" style="user-select: none;" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: .5rem; text-underline-offset: 3px;">
|
<div style="display: flex; align-items: center; gap: .5rem; text-underline-offset: 3px;">
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest" rel="noopener noreferer">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest" rel="noopener noreferer">
|
||||||
Checkout latest changelogs
|
Checkout latest changelogs
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<span style="font-size: .9rem;">•</span>
|
<span style="font-size: .9rem;">•</span>
|
||||||
|
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api#documentation" rel="noopener noreferer">
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api#documentation" rel="noopener noreferer">
|
||||||
Visit docs for more information
|
Visit docs for more information
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="contributors">
|
<div class="contributors">
|
||||||
<p style="text-wrap: balance; line-height: 1.2;">Thanks to the following people for keeping this
|
<p style="text-wrap: balance; line-height: 1.2;">Thanks to the following people for keeping this
|
||||||
project alive and
|
project alive and
|
||||||
relevant.</p>
|
relevant.</p>
|
||||||
|
|
||||||
<a href="https://github.com/ghoshRitesh12/aniwatch-api/graphs/contributors"
|
<a href="https://github.com/ghoshRitesh12/aniwatch-api/graphs/contributors"
|
||||||
style="display: block; margin-bottom: 1rem; user-select: none;">
|
style="display: block; margin-bottom: 1rem; user-select: none;">
|
||||||
<img src="https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch-api" alt="Contributors" decoding="async">
|
<img src="https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch-api" alt="Contributors"
|
||||||
</a>
|
decoding="async">
|
||||||
</div>
|
</a>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+25
-18
@@ -1,47 +1,54 @@
|
|||||||
import { config } from "dotenv";
|
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
import { env } from "./env.js";
|
||||||
config();
|
|
||||||
|
|
||||||
export class AniwatchAPICache {
|
export class AniwatchAPICache {
|
||||||
private _client: Redis | null;
|
private static instance: AniwatchAPICache | null = null;
|
||||||
|
|
||||||
|
private client: Redis | null;
|
||||||
public isOptional: boolean = true;
|
public isOptional: boolean = true;
|
||||||
|
|
||||||
static DEFAULT_CACHE_EXPIRY_SECONDS = 60 as const;
|
static DEFAULT_CACHE_EXPIRY_SECONDS = 60 as const;
|
||||||
static CACHE_EXPIRY_HEADER_NAME = "X-ANIWATCH-CACHE-EXPIRY" as const;
|
static CACHE_EXPIRY_HEADER_NAME = "X-ANIWATCH-CACHE-EXPIRY" as const;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const redisConnURL = process.env?.ANIWATCH_API_REDIS_CONN_URL;
|
const redisConnURL = env.ANIWATCH_API_REDIS_CONN_URL;
|
||||||
this.isOptional = !Boolean(redisConnURL);
|
this.isOptional = !Boolean(redisConnURL);
|
||||||
this._client = this.isOptional ? null : new Redis(String(redisConnURL));
|
this.client = this.isOptional ? null : new Redis(String(redisConnURL));
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: string | Buffer, value: string | Buffer | number) {
|
static getInstance() {
|
||||||
if (this.isOptional) return;
|
if (!AniwatchAPICache.instance) {
|
||||||
return this._client?.set(key, value);
|
AniwatchAPICache.instance = new AniwatchAPICache();
|
||||||
|
}
|
||||||
|
return AniwatchAPICache.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: string | Buffer) {
|
// set(key: string | Buffer, value: string | Buffer | number) {
|
||||||
if (this.isOptional) return;
|
// if (this.isOptional) return;
|
||||||
return this._client?.get(key);
|
// return this.client?.set(key, value);
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// get(key: string | Buffer) {
|
||||||
|
// if (this.isOptional) return;
|
||||||
|
// return this.client?.get(key);
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param expirySeconds set to 60 by default
|
* @param expirySeconds set to 60 by default
|
||||||
*/
|
*/
|
||||||
async getOrSet<T>(
|
async getOrSet<T>(
|
||||||
setCB: () => Promise<T>,
|
dataGetter: () => Promise<T>,
|
||||||
key: string | Buffer,
|
key: string | Buffer,
|
||||||
expirySeconds: number = AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS
|
expirySeconds: number = AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS
|
||||||
) {
|
) {
|
||||||
const cachedData = this.isOptional
|
const cachedData = this.isOptional
|
||||||
? null
|
? null
|
||||||
: (await this._client?.get(key)) || null;
|
: (await this.client?.get?.(key)) || null;
|
||||||
let data = JSON.parse(String(cachedData)) as T;
|
let data = JSON.parse(String(cachedData)) as T;
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = await setCB();
|
data = await dataGetter();
|
||||||
await this._client?.set(
|
await this.client?.set?.(
|
||||||
key,
|
key,
|
||||||
JSON.stringify(data),
|
JSON.stringify(data),
|
||||||
"EX",
|
"EX",
|
||||||
@@ -52,4 +59,4 @@ export class AniwatchAPICache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cache = new AniwatchAPICache();
|
export const cache = AniwatchAPICache.getInstance();
|
||||||
|
|||||||
+10
-12
@@ -1,17 +1,15 @@
|
|||||||
import { config } from "dotenv";
|
|
||||||
import { cors } from "hono/cors";
|
import { cors } from "hono/cors";
|
||||||
|
import { env } from "./env.js";
|
||||||
|
|
||||||
config();
|
const DEFAULT_ALLOWED_ORIGINS = ["http://localhost:4000", "*"];
|
||||||
|
|
||||||
const allowedOrigins = process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS
|
const allowedOrigins = env.ANIWATCH_API_CORS_ALLOWED_ORIGINS
|
||||||
? process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS.split(",")
|
? env.ANIWATCH_API_CORS_ALLOWED_ORIGINS.split(",")
|
||||||
: ["http://localhost:4000", "*"];
|
: DEFAULT_ALLOWED_ORIGINS;
|
||||||
|
|
||||||
const corsConfig = cors({
|
export const corsConfig = cors({
|
||||||
allowMethods: ["GET"],
|
allowMethods: ["GET"],
|
||||||
maxAge: 600,
|
maxAge: 600,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
origin: allowedOrigins,
|
origin: allowedOrigins,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default corsConfig;
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { config } from "dotenv";
|
||||||
|
import { cleanEnv, num, str, bool, url, port } from "envalid";
|
||||||
|
|
||||||
|
config();
|
||||||
|
|
||||||
|
export const env = cleanEnv(process.env, {
|
||||||
|
ANIWATCH_API_PORT: port({
|
||||||
|
default: 4000,
|
||||||
|
desc: "Port number of the Aniwatch API.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_WINDOW_MS: num({
|
||||||
|
// 60 mins if dev, else 30 mins
|
||||||
|
default: isDevEnv() ? 60 * 60 * 1000 : 30 * 60 * 1000,
|
||||||
|
desc: "Duration to track requests for rate limiting (in milliseconds).",
|
||||||
|
docs: "https://github.com/ghoshRitesh12/aniwatch-api/blob/main/.env.example#L9",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_MAX_REQS: num({
|
||||||
|
default: isDevEnv() ? 600 : 6,
|
||||||
|
desc: "Maximum number of requests in the `ANIWATCH_API_WINDOW_MS` time period.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_CORS_ALLOWED_ORIGINS: str({
|
||||||
|
default: undefined,
|
||||||
|
example:
|
||||||
|
"https://your-production-domain.com,https://another-trusted-domain.com",
|
||||||
|
desc: "Allowed origins, separated by commas and no spaces in between (CSV).",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_VERCEL_DEPLOYMENT: bool({
|
||||||
|
default: false,
|
||||||
|
desc: "Required for distinguishing Vercel deployment from other ones; set it to true",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_HOSTNAME: url({
|
||||||
|
default: undefined,
|
||||||
|
example: "https://your-production-domain.com",
|
||||||
|
desc: "Set this to your api instance's hostname to enable rate limiting, don't have this value if you don't wish to rate limit.",
|
||||||
|
docs: "https://github.com/ghoshRitesh12/aniwatch-api/blob/main/.env.example#L18",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_REDIS_CONN_URL: url({
|
||||||
|
default: undefined,
|
||||||
|
example:
|
||||||
|
"rediss://default:your-secure-password@your-redis-instance-name.provider.com:6379",
|
||||||
|
desc: "This env is optional by default and can be set to utilize Redis caching functionality. It has to be a valid connection URL.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_S_MAXAGE: num({
|
||||||
|
default: 60,
|
||||||
|
desc: "Specifies the maximum amount of time (in seconds) a resource is considered fresh when served by a CDN cache.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
ANIWATCH_API_STALE_WHILE_REVALIDATE: num({
|
||||||
|
default: 30,
|
||||||
|
desc: "Specifies the amount of time (in seconds) a resource is served stale while a new one is fetched.",
|
||||||
|
}),
|
||||||
|
|
||||||
|
NODE_ENV: str({
|
||||||
|
default: "development",
|
||||||
|
choices: ["development", "production", "test", "staging"],
|
||||||
|
desc: "The environment in which the application is running.",
|
||||||
|
docs: "https://nodejs.org/en/learn/getting-started/nodejs-the-difference-between-development-and-production",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
function isDevEnv(): boolean {
|
||||||
|
return (
|
||||||
|
!process.env.NODE_ENV ||
|
||||||
|
process.env.NODE_ENV === "development" ||
|
||||||
|
process.env.NODE_ENV === "test"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEnvUndefined(envVar?: string): boolean {
|
||||||
|
return typeof envVar === "undefined" || envVar === "undefined";
|
||||||
|
}
|
||||||
+13
-12
@@ -1,27 +1,28 @@
|
|||||||
import { HiAnimeError } from "aniwatch";
|
import { HiAnimeError } from "aniwatch";
|
||||||
import type { ErrorHandler, NotFoundHandler } from "hono";
|
import type { ErrorHandler, NotFoundHandler } from "hono";
|
||||||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||||
|
import { log } from "./logger.js";
|
||||||
|
|
||||||
const errResp: { status: ContentfulStatusCode; message: string } = {
|
const errResp: { status: ContentfulStatusCode; message: string } = {
|
||||||
status: 500,
|
status: 500,
|
||||||
message: "Internal Server Error",
|
message: "Internal Server Error",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const errorHandler: ErrorHandler = (err, c) => {
|
export const errorHandler: ErrorHandler = (err, c) => {
|
||||||
console.error(err);
|
log.error(JSON.stringify(err));
|
||||||
|
|
||||||
if (err instanceof HiAnimeError) {
|
if (err instanceof HiAnimeError) {
|
||||||
errResp.status = err.status as ContentfulStatusCode;
|
errResp.status = err.status as ContentfulStatusCode;
|
||||||
errResp.message = err.message;
|
errResp.message = err.message;
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(errResp, errResp.status);
|
return c.json(errResp, errResp.status);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notFoundHandler: NotFoundHandler = (c) => {
|
export const notFoundHandler: NotFoundHandler = (c) => {
|
||||||
errResp.status = 404;
|
errResp.status = 404;
|
||||||
errResp.message = "Not Found";
|
errResp.message = "Not Found";
|
||||||
|
|
||||||
console.error(errResp);
|
log.error(JSON.stringify(errResp));
|
||||||
return c.json(errResp, errResp.status);
|
return c.json(errResp, errResp.status);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { env } from "./env.js";
|
||||||
|
import { pino, type LoggerOptions } from "pino";
|
||||||
|
|
||||||
|
const loggerOptions: LoggerOptions = {
|
||||||
|
redact: env.isProduction ? ["hostname"] : [],
|
||||||
|
level: "info",
|
||||||
|
transport: env.isDev
|
||||||
|
? {
|
||||||
|
target: "pino-pretty",
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
translateTime: "SYS:standard",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
formatters: {
|
||||||
|
level(label) {
|
||||||
|
return {
|
||||||
|
level: label.toUpperCase(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timestamp: pino.stdTimeFunctions.isoTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const log = pino(loggerOptions);
|
||||||
+16
-15
@@ -1,21 +1,22 @@
|
|||||||
import { config } from "dotenv";
|
|
||||||
import { rateLimiter } from "hono-rate-limiter";
|
import { rateLimiter } from "hono-rate-limiter";
|
||||||
import { getConnInfo } from "@hono/node-server/conninfo";
|
import { getConnInfo } from "@hono/node-server/conninfo";
|
||||||
|
import { env } from "./env.js";
|
||||||
config();
|
|
||||||
|
|
||||||
export const ratelimit = rateLimiter({
|
export const ratelimit = rateLimiter({
|
||||||
windowMs: Number(process.env.ANIWATCH_API_WINDOW_MS) || 30 * 60 * 1000,
|
windowMs: env.ANIWATCH_API_WINDOW_MS,
|
||||||
limit: Number(process.env.ANIWATCH_API_MAX_REQS) || 6,
|
limit: env.ANIWATCH_API_MAX_REQS,
|
||||||
standardHeaders: "draft-7",
|
standardHeaders: "draft-7",
|
||||||
keyGenerator(c) {
|
keyGenerator(c) {
|
||||||
const { remote } = getConnInfo(c);
|
const { remote } = getConnInfo(c);
|
||||||
const key =
|
const key =
|
||||||
`${String(remote.addressType)}_` +
|
`${String(remote.addressType)}_` +
|
||||||
`${String(remote.address)}:${String(remote.port)}`;
|
`${String(remote.address)}:${String(remote.port)}`;
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
},
|
},
|
||||||
handler: (c) =>
|
handler: (c) =>
|
||||||
c.json({ status: 429, message: "Too Many Requests 😵" }, { status: 429 }),
|
c.json(
|
||||||
|
{ status: 429, message: "Too Many Requests 😵" },
|
||||||
|
{ status: 429 }
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
type CacheVariables = {
|
type CacheVariables = {
|
||||||
CACHE_CONFIG: {
|
CACHE_CONFIG: {
|
||||||
key: string;
|
key: string;
|
||||||
duration: number;
|
duration: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AniwatchAPIVariables = {} & CacheVariables;
|
export type AniwatchAPIVariables = CacheVariables & {};
|
||||||
|
|||||||
+21
-24
@@ -1,35 +1,32 @@
|
|||||||
import { config } from "dotenv";
|
|
||||||
import { AniwatchAPICache } from "../config/cache.js";
|
|
||||||
import type { MiddlewareHandler } from "hono";
|
import type { MiddlewareHandler } from "hono";
|
||||||
|
import { env } from "../config/env.js";
|
||||||
config();
|
import { AniwatchAPICache } from "../config/cache.js";
|
||||||
|
|
||||||
// Define middleware to add Cache-Control header
|
// Define middleware to add Cache-Control header
|
||||||
export const cacheControlMiddleware: MiddlewareHandler = async (c, next) => {
|
export const cacheControl: MiddlewareHandler = async (c, next) => {
|
||||||
const sMaxAge = process.env.ANIWATCH_API_S_MAXAGE || "60";
|
const sMaxAge = env.ANIWATCH_API_S_MAXAGE;
|
||||||
const staleWhileRevalidate =
|
const staleWhileRevalidate = env.ANIWATCH_API_STALE_WHILE_REVALIDATE;
|
||||||
process.env.ANIWATCH_API_STALE_WHILE_REVALIDATE || "30";
|
|
||||||
|
|
||||||
c.header(
|
c.header(
|
||||||
"Cache-Control",
|
"Cache-Control",
|
||||||
`s-maxage=${sMaxAge}, stale-while-revalidate=${staleWhileRevalidate}`
|
`s-maxage=${sMaxAge}, stale-while-revalidate=${staleWhileRevalidate}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export function cacheConfigSetter(keySliceIndex: number): MiddlewareHandler {
|
export function cacheConfigSetter(keySliceIndex: number): MiddlewareHandler {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const { pathname, search } = new URL(c.req.url);
|
const { pathname, search } = new URL(c.req.url);
|
||||||
|
|
||||||
c.set("CACHE_CONFIG", {
|
c.set("CACHE_CONFIG", {
|
||||||
key: `${pathname.slice(keySliceIndex) + search}`,
|
key: `${pathname.slice(keySliceIndex) + search}`,
|
||||||
duration: Number(
|
duration: Number(
|
||||||
c.req.header(AniwatchAPICache.CACHE_EXPIRY_HEADER_NAME) ||
|
c.req.header(AniwatchAPICache.CACHE_EXPIRY_HEADER_NAME) ||
|
||||||
AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS
|
AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { MiddlewareHandler } from "hono";
|
||||||
|
import { logger as honoLogger } from "hono/logger";
|
||||||
|
import { log } from "../config/logger.js";
|
||||||
|
|
||||||
|
export const logging: MiddlewareHandler = honoLogger(
|
||||||
|
(msg: string, ...rest: string[]) => {
|
||||||
|
log.info(msg, ...rest);
|
||||||
|
}
|
||||||
|
);
|
||||||
+28
-35
@@ -1,40 +1,36 @@
|
|||||||
import https from "https";
|
import https from "https";
|
||||||
import { config } from "dotenv";
|
|
||||||
|
|
||||||
import corsConfig from "./config/cors.js";
|
|
||||||
import { ratelimit } from "./config/ratelimit.js";
|
|
||||||
|
|
||||||
import {
|
|
||||||
cacheConfigSetter,
|
|
||||||
cacheControlMiddleware,
|
|
||||||
} from "./middleware/cache.js";
|
|
||||||
import { hianimeRouter } from "./routes/hianime.js";
|
|
||||||
|
|
||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { logger } from "hono/logger";
|
|
||||||
import { serve } from "@hono/node-server";
|
import { serve } from "@hono/node-server";
|
||||||
import { serveStatic } from "@hono/node-server/serve-static";
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
|
|
||||||
import pkgJson from "../package.json" with { type: "json" };
|
import { env } from "./config/env.js";
|
||||||
|
import { log } from "./config/logger.js";
|
||||||
|
import { corsConfig } from "./config/cors.js";
|
||||||
|
import { ratelimit } from "./config/ratelimit.js";
|
||||||
import { errorHandler, notFoundHandler } from "./config/errorHandler.js";
|
import { errorHandler, notFoundHandler } from "./config/errorHandler.js";
|
||||||
import type { AniwatchAPIVariables } from "./config/variables.js";
|
import type { AniwatchAPIVariables } from "./config/variables.js";
|
||||||
|
|
||||||
config();
|
import { hianimeRouter } from "./routes/hianime.js";
|
||||||
|
import { logging } from "./middleware/logging.js";
|
||||||
|
import { cacheConfigSetter, cacheControl } from "./middleware/cache.js";
|
||||||
|
|
||||||
|
import pkgJson from "../package.json" with { type: "json" };
|
||||||
|
|
||||||
|
//
|
||||||
const BASE_PATH = "/api/v2" as const;
|
const BASE_PATH = "/api/v2" as const;
|
||||||
const PORT: number = Number(process.env.ANIWATCH_API_PORT) || 4000;
|
|
||||||
const ANIWATCH_API_HOSTNAME = process.env?.ANIWATCH_API_HOSTNAME;
|
|
||||||
|
|
||||||
const app = new Hono<{ Variables: AniwatchAPIVariables }>();
|
const app = new Hono<{ Variables: AniwatchAPIVariables }>();
|
||||||
|
|
||||||
app.use(logger());
|
app.use(logging);
|
||||||
app.use(corsConfig);
|
app.use(corsConfig);
|
||||||
app.use(cacheControlMiddleware);
|
app.use(cacheControl);
|
||||||
|
|
||||||
|
//
|
||||||
// CAUTION: For personal deployments, "refrain" from having an env
|
// CAUTION: For personal deployments, "refrain" from having an env
|
||||||
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting
|
// named "ANIWATCH_API_HOSTNAME". You may face rate limitting
|
||||||
// or other issues if you do.
|
// or other issues if you do.
|
||||||
const ISNT_PERSONAL_DEPLOYMENT = Boolean(ANIWATCH_API_HOSTNAME);
|
const ISNT_PERSONAL_DEPLOYMENT = Boolean(env.ANIWATCH_API_HOSTNAME);
|
||||||
if (ISNT_PERSONAL_DEPLOYMENT) {
|
if (ISNT_PERSONAL_DEPLOYMENT) {
|
||||||
app.use(ratelimit);
|
app.use(ratelimit);
|
||||||
}
|
}
|
||||||
@@ -43,7 +39,8 @@ app.use("/", serveStatic({ root: "public" }));
|
|||||||
app.get("/health", (c) => c.text("daijoubu", { status: 200 }));
|
app.get("/health", (c) => c.text("daijoubu", { status: 200 }));
|
||||||
app.get("/v", async (c) =>
|
app.get("/v", async (c) =>
|
||||||
c.text(
|
c.text(
|
||||||
`v${"version" in pkgJson && pkgJson?.version ? pkgJson.version : "-1"}`
|
`aniwatch-api: v${"version" in pkgJson && pkgJson?.version ? pkgJson.version : "-1"}\n` +
|
||||||
|
`aniwatch-package: v${"dependencies" in pkgJson && pkgJson?.dependencies?.aniwatch ? pkgJson?.dependencies?.aniwatch : "-1"}`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -57,18 +54,17 @@ app.basePath(BASE_PATH).get("/anicrush", (c) =>
|
|||||||
app.notFound(notFoundHandler);
|
app.notFound(notFoundHandler);
|
||||||
app.onError(errorHandler);
|
app.onError(errorHandler);
|
||||||
|
|
||||||
|
//
|
||||||
// NOTE: this env is "required" for vercel deployments
|
// NOTE: this env is "required" for vercel deployments
|
||||||
if (!Boolean(process.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) {
|
if (!env.ANIWATCH_API_VERCEL_DEPLOYMENT) {
|
||||||
serve({
|
serve({
|
||||||
port: PORT,
|
port: env.ANIWATCH_API_PORT,
|
||||||
fetch: app.fetch,
|
fetch: app.fetch,
|
||||||
}).addListener("listening", () =>
|
}).addListener("listening", () => {
|
||||||
console.info(
|
log.info(
|
||||||
"\x1b[1;36m" +
|
`aniwatch-api RUNNING at http://localhost:${env.ANIWATCH_API_PORT}`
|
||||||
`aniwatch-api at http://localhost:${PORT}` +
|
);
|
||||||
"\x1b[0m"
|
});
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// NOTE: remove the `if` block below for personal deployments
|
// NOTE: remove the `if` block below for personal deployments
|
||||||
if (ISNT_PERSONAL_DEPLOYMENT) {
|
if (ISNT_PERSONAL_DEPLOYMENT) {
|
||||||
@@ -76,15 +72,12 @@ if (!Boolean(process.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) {
|
|||||||
|
|
||||||
// don't sleep
|
// don't sleep
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
console.log(
|
log.info(
|
||||||
"aniwatch-api HEALTH_CHECK at",
|
`aniwatch-api HEALTH_CHECK at ${new Date().toISOString()}`
|
||||||
new Date().toISOString()
|
|
||||||
);
|
);
|
||||||
https
|
https
|
||||||
.get(`https://${ANIWATCH_API_HOSTNAME}/health`)
|
.get(`https://${env.ANIWATCH_API_HOSTNAME}/health`)
|
||||||
.on("error", (err) => {
|
.on("error", (err) => log.error(err.message.trim()));
|
||||||
console.error(err.message);
|
|
||||||
});
|
|
||||||
}, interval);
|
}, interval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { HiAnime } from "aniwatch";
|
|
||||||
import { expect, test } from "vitest";
|
|
||||||
|
|
||||||
const animeId = "steinsgate-3";
|
|
||||||
|
|
||||||
// npx vitest run animeAboutInfo.test.ts
|
|
||||||
test(`GET /api/v2/hianime/anime/${animeId}`, async () => {
|
|
||||||
const hianime = new HiAnime.Scraper();
|
|
||||||
const data = await hianime.getInfo(animeId);
|
|
||||||
|
|
||||||
expect(data.anime.info.name).not.toEqual(null);
|
|
||||||
expect(data.recommendedAnimes).not.toEqual([]);
|
|
||||||
expect(data.mostPopularAnimes).not.toEqual([]);
|
|
||||||
expect(Object.keys(data.anime.moreInfo)).not.toEqual([]);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { expect, test } from "vitest";
|
|
||||||
import { HiAnime } from "aniwatch";
|
|
||||||
|
|
||||||
const category = "subbed-anime";
|
|
||||||
|
|
||||||
// npx vitest run animeCategory.test.ts
|
|
||||||
test(`GET /api/v2/hianime/category/${category}`, async () => {
|
|
||||||
const hianime = new HiAnime.Scraper();
|
|
||||||
const data = await hianime.getCategoryAnime(category);
|
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
|
||||||
expect(data.genres).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.today).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.week).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.month).not.toEqual([]);
|
|
||||||
});
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { expect, test } from "vitest";
|
|
||||||
import { HiAnime } from "aniwatch";
|
|
||||||
|
|
||||||
const producerName = "toei-animation";
|
|
||||||
const page = 2;
|
|
||||||
|
|
||||||
// npx vitest run animeProducer.test.ts
|
|
||||||
test(`GET /api/v2/hianime/producer/${producerName}?page=${page}`, async () => {
|
|
||||||
const hianime = new HiAnime.Scraper();
|
|
||||||
const data = await hianime.getProducerAnimes(producerName, page);
|
|
||||||
|
|
||||||
expect(data.animes).not.toEqual([]);
|
|
||||||
expect(data.topAiringAnimes).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.today).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.week).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.month).not.toEqual([]);
|
|
||||||
});
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { expect, test } from "vitest";
|
|
||||||
import { HiAnime } from "aniwatch";
|
|
||||||
|
|
||||||
const animeEpisodeId = "steinsgate-0-92?ep=2055";
|
|
||||||
|
|
||||||
// npx vitest run episodeServers.test.ts
|
|
||||||
test(`GET /api/v2/hianime/episode/servers?animeEpisodeId=${animeEpisodeId}`, async () => {
|
|
||||||
const hianime = new HiAnime.Scraper();
|
|
||||||
const data = await hianime.getEpisodeServers(animeEpisodeId);
|
|
||||||
|
|
||||||
expect(data.episodeId).not.toEqual(null);
|
|
||||||
expect(data.episodeNo).not.toEqual(0);
|
|
||||||
expect(data.sub).not.toEqual([]);
|
|
||||||
expect(data.dub).not.toEqual([]);
|
|
||||||
});
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { expect, test } from "vitest";
|
|
||||||
import { HiAnime } from "aniwatch";
|
|
||||||
|
|
||||||
test("GET /api/v2/hianime/home", async () => {
|
|
||||||
const hianime = new HiAnime.Scraper();
|
|
||||||
const data = await hianime.getHomePage();
|
|
||||||
|
|
||||||
expect(data.spotlightAnimes).not.toEqual([]);
|
|
||||||
expect(data.trendingAnimes).not.toEqual([]);
|
|
||||||
expect(data.latestEpisodeAnimes).not.toEqual([]);
|
|
||||||
expect(data.topUpcomingAnimes).not.toEqual([]);
|
|
||||||
expect(data.topAiringAnimes).not.toEqual([]);
|
|
||||||
expect(data.mostPopularAnimes).not.toEqual([]);
|
|
||||||
expect(data.mostFavoriteAnimes).not.toEqual([]);
|
|
||||||
expect(data.latestCompletedAnimes).not.toEqual([]);
|
|
||||||
expect(data.genres).not.toEqual([]);
|
|
||||||
|
|
||||||
expect(data.top10Animes.today).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.week).not.toEqual([]);
|
|
||||||
expect(data.top10Animes.month).not.toEqual([]);
|
|
||||||
});
|
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
name: "aniwatch-api",
|
name: "aniwatch-api",
|
||||||
environment: "node",
|
environment: "node",
|
||||||
testTimeout: 15000,
|
testTimeout: 15000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user