diff options
| author | Ritesh Ghosh <[email protected]> | 2024-10-06 01:13:23 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-10-06 01:13:23 +0530 |
| commit | 46f688ac12a99b8fb145b0745dd4cc6babff1e1e (patch) | |
| tree | 9560dd057822069a2162ef01a1118f3ac05e6d07 | |
| parent | 55810ccf2372209f9b46c96ac12811e5a05f7961 (diff) | |
| download | aniwatch-api-46f688ac12a99b8fb145b0745dd4cc6babff1e1e.tar.xz aniwatch-api-46f688ac12a99b8fb145b0745dd4cc6babff1e1e.zip | |
Aniwatch API Version 2 (#66)
BREAKING CHANGE:
* chore: remove files that are not necessary for api v2
* test: update existing tests to use pkg
* feat: organized aniwatch api envs and add more info about them
* feat: update tsconfig to include strict noUnsed params
* feat(api homepage): revamp api home page
* feat: update wani kuni image
* feat: add dot img
* feat: use hono cors
* feat: use hono rate limiter
* build: remove unnecessary deps, add ones needed and update description
* feat: add hianime routes and their handlers
* feat: update vercel deployment file
* docs: update logo and scraper docs, add envs section
* feat: update main server file
* feat: update peronal deployments caution section
84 files changed, 1027 insertions, 3892 deletions
diff --git a/.env.example b/.env.example index 817a534..c9e59b3 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,23 @@ -DOMAIN="aniwatchtv.to" -PORT=4000 -CORS_ALLOWED_ORIGINS=https://your-production-domain.com,https://another-trusted-domain.com +# port number of the aniwatch api +ANIWATCH_API_PORT=4000 + +# env to control allowed origins +ANIWATCH_API_CORS_ALLOWED_ORIGINS=https://your-production-domain.com,https://another-trusted-domain.com # RATE LIMIT -WINDOW_MS=1800000 # duration to track requests (in milliseconds) for rate limiting. here, 30*60*1000 = 1800000 = 30 minutes -MAX=70 # maximum number of requests in this timeperiod +# duration to track requests (in milliseconds) for rate limiting. here, 30*60*1000 = 1800000 = 30 minutes +ANIWATCH_API_WINDOW_MS=1800000 +# maximum number of requests in this timeperiod +ANIWATCH_API_MAX_REQS=70 + +# CAUTION: +# For personal deployments, if you wanna have rate limitting +# in your application, then set this env to your deployed +# instance's hostname, otherwise don't set or have this env at all. +# If you set this env to an incorrect value, you may face other issues. + +# ANIWATCH_API_HOSTNAME="api-aniwatch.onrender.com" + + +# NOTE: this env is "required" for vercel deployments +# ANIWATCH_VERCEL_DEPLOYMENT=<true or any non zero value> @@ -1,10 +1,10 @@ <p align="center"> <a href="https://github.com/ghoshRitesh12/aniwatch-api"> <img - src="https://github.com/ghoshRitesh12/aniwatch-api/blob/main/public/img/img1.gif?raw=true" - alt="Logo" - width="120" - height="120" + src="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png" + alt="aniwatch_logo" + width="175" + height="175" decoding="async" fetchpriority="high" /> @@ -51,6 +51,9 @@ </div> +> [!NOTE] +> The API has shifted to version 2, thereby naturally introducing breaking changes. If you'd want to use the previous version of this API, then refer to [the last stable version](https://github.com/ghoshRitesh12/aniwatch-api/tree/v1.40.0). + > [!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 minimise bandwidth consumption. It is recommended to deploy your own instance for personal use by customizing the api as you need it to be. @@ -62,6 +65,7 @@ - [Installation](#installation) - [Local](#local) - [Docker](#docker) +- [Envs](#envs) - [Host your instance](#-host-your-instance) - [Vercel](#vercel) - [Render](#render) @@ -72,7 +76,7 @@ - [GET Search Suggestions](#get-search-suggestions) - [GET Producer Animes](#get-producer-animes) - [GET Genre Animes](#get-genre-animes) - - [GET Category Anime](#get-category-anime) + - [GET Category Animes](#get-category-animes) - [GET Estimated Schedules](#get-estimated-schedules) - [GET Anime Episodes](#get-anime-episodes) - [GET Anime Episode Servers](#get-anime-episode-servers) @@ -124,11 +128,25 @@ The above command will start the server on port 4000. You can access the server You can also add the `-d` flag to run the container in detached mode. +## <span id="envs">⚙️ Envs</span> + +More info can be found in [`.env.example`](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/.env.example) file + +- `ANIWATCH_API_PORT`: port number of the aniwatch api +- `ANIWATCH_API_WINDOW_MS`: duration to track requests for rate limitting (in milliseconds) +- `ANIWATCH_API_MAX_REQS`: maximum number of requests in the `ANIWATCH_API_WINDOW_MS` timeperiod +- `ANIWATCH_API_CORS_ALLOWED_ORIGINS`: allowed origins, separated by commas and no spaces in between +- `ANIWATCH_VERCEL_DEPLOYMENT`: required for distinguishing vercel deployment from other ones, set it to true of any other non-zero value +- `ANIWATCH_API_HOSTNAME`: set this to your api instance's hostname to enable rate limitting, don't have this value if you don't wish to rate limit + ## <span id="host-your-instance">⛅ Host your instance</span> > [!CAUTION] > -> For personal deployments, **refrain from having an env** named `ANIWATCH_API_HOSTNAME`. You may face rate limitting and other issues if you do. +> For personal deployments: +> +> - If you wanna have rate limitting in your application, then set the `ANIWATCH_API_HOSTNAME` env to your deployed instance's hostname, otherwise don't set or have this env at all. If you set this env to an incorrect value, you may face other issues. +> - Remove the if block from the [`server.ts`](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts) file, spanning from lines [60](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts#L60) to [84](https://github.com/ghoshRitesh12/aniwatch-api/blob/main/src/server.ts#L84). ### Vercel @@ -138,7 +156,7 @@ Deploy your own instance of Aniwatch API on Vercel. > [!NOTE] > -> When deploying to vercel, set an env named `IS_VERCEL_DEPLOYMENT` to `true` or anything, but this env should be present. +> When deploying to vercel, set an env named `ANIWATCH_VERCEL_DEPLOYMENT` to `true` or any non-zero value, but this env must be present. ### Render @@ -161,13 +179,13 @@ The endpoints exposed by the api are listed below with examples that uses the [F #### Endpoint ```bash -https://api-aniwatch.onrender.com/anime/home +/api/v2/hianime/home ``` #### Request sample ```javascript -const resp = await fetch("https://api-aniwatch.onrender.com/anime/home"); +const resp = await fetch("/api/v2/hianime/home"); const data = await resp.json(); console.log(data); ``` @@ -176,125 +194,128 @@ console.log(data); ```javascript { - genres: ["Action", "Cars", "Adventure", ...], - latestEpisodeAnimes: [ - { - id: string, - name: string, - poster: string, - type: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - spotlightAnimes: [ - { - id: string, - name: string, - jname: string, - poster: string, - description: string, - rank: number, - otherInfo: string[], - episodes: { - sub: number, - dub: number, + success: true, + data: { + genres: ["Action", "Cars", "Adventure", ...], + latestEpisodeAnimes: [ + { + id: string, + name: string, + poster: string, + type: string, + episodes: { + sub: number, + dub: number, + } }, - }, - {...}, - ], - top10Animes: { - today: [ + {...}, + ], + spotlightAnimes: [ { + id: string, + name: string, + jname: string, + poster: string, + description: string, + rank: number, + otherInfo: string[], episodes: { sub: number, dub: number, }, + }, + {...}, + ], + top10Animes: { + today: [ + { + episodes: { + sub: number, + dub: number, + }, + id: string, + name: string, + poster: string, + rank: number + }, + {...}, + ], + month: [...], + week: [...] + }, + topAiringAnimes: [ + { id: string, name: string, + jname: string, poster: string, - rank: number }, {...}, ], - month: [...], - week: [...] - }, - topAiringAnimes: [ - { - id: string, - name: string, - jname: string, - poster: string, - }, - {...}, - ], - topUpcomingAnimes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - trendingAnimes: [ - { - id: string, - name: string, - poster: string, - rank: number, - }, - {...}, - ], - mostPopularAnimes: [ - { - id: string, - name: string, - poster: string, - type: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - mostFavoriteAnimes: [ - { - id: string, - name: string, - poster: string, - type: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - latestCompletedAnimes: [ - { - id: string, - name: string, - poster: string, - type: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], + topUpcomingAnimes: [ + { + id: string, + name: string, + poster: string, + duration: string, + type: string, + rating: string, + episodes: { + sub: number, + dub: number, + } + }, + {...}, + ], + trendingAnimes: [ + { + id: string, + name: string, + poster: string, + rank: number, + }, + {...}, + ], + mostPopularAnimes: [ + { + id: string, + name: string, + poster: string, + type: string, + episodes: { + sub: number, + dub: number, + } + }, + {...}, + ], + mostFavoriteAnimes: [ + { + id: string, + name: string, + poster: string, + type: string, + episodes: { + sub: number, + dub: number, + } + }, + {...}, + ], + latestCompletedAnimes: [ + { + id: string, + name: string, + poster: string, + type: string, + episodes: { + sub: number, + dub: number, + } + }, + {...}, + ], + } } ``` @@ -314,21 +335,19 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/info?id={anime-id} +/api/v2/hianime/anime/{animeId} ``` #### Query Parameters | Parameter | Type | Description | Required? | Default | | :-------: | :----: | :----------------------------------: | :-------: | :-----: | -| `id` | string | The unique anime id (in kebab case). | Yes | -- | +| `animeId` | string | The unique anime id (in kebab case). | Yes | -- | #### Request sample ```javascript -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/info?id=attack-on-titan-112" -); +const resp = await fetch("/api/v2/hianime/anime/attack-on-titan-112"); const data = await resp.json(); console.log(data); ``` @@ -337,111 +356,114 @@ console.log(data); ```javascript { - anime: [ - info: { - id: string, - name: string, - poster: string, - description: string, - stats: { - rating: string, - quality: string, + success: true, + data: { + anime: [ + info: { + id: string, + name: string, + poster: string, + description: string, + stats: { + rating: string, + quality: string, + episodes: { + sub: number, + dub: number + }, + type: string, + duration: string + }, + promotionalVideos: [ + { + title: string | undefined, + source: string | undefined, + thumbnail: string | undefined + }, + {...}, + ], + characterVoiceActor: [ + { + character: { + id: string, + poster: string, + name: string, + cast: string + }, + voiceActor: { + id: string, + poster: string, + name: string, + cast: string + } + }, + {...}, + ] + } + moreInfo: { + aired: string, + genres: ["Action", "Mystery", ...], + status: string, + studios: string, + duration: string + ... + } + ], + mostPopularAnimes: [ + { episodes: { sub: number, - dub: number + dub: number, }, + id: string, + jname: string, + name: string, + poster: string, + type: string + }, + {...}, + ], + recommendedAnimes: [ + { + id: string, + name: string, + poster: string, + duration: string, type: string, - duration: string + rating: string, + episodes: { + sub: number, + dub: number, + } }, - promotionalVideos: [ - { - title: string | undefined, - source: string | undefined, - thumbnail: string | undefined - }, - {...}, - ], - characterVoiceActor: [ - { - character: { - id: string, - poster: string, - name: string, - cast: string - }, - voiceActor: { - id: string, - poster: string, - name: string, - cast: string - } - }, - {...}, - ] - } - moreInfo: { - aired: string, - genres: ["Action", "Mystery", ...], - status: string, - studios: string, - duration: string - ... - } - ], - mostPopularAnimes: [ - { - episodes: { - sub: number, - dub: number, + {...}, + ], + relatedAnimes: [ + { + id: string, + name: string, + poster: string, + duration: string, + type: string, + rating: string, + episodes: { + sub: number, + dub: number, + } }, - id: string, - jname: string, - name: string, - poster: string, - type: string - }, - {...}, - ], - recommendedAnimes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - relatedAnimes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - seasons: [ - { - id: string, - name: string, - title: string, - poster: string, - isCurrent: boolean - }, - {...} - ] + {...}, + ], + seasons: [ + { + id: string, + name: string, + title: string, + poster: string, + isCurrent: boolean + }, + {...} + ] + } } ``` @@ -461,10 +483,10 @@ console.log(data); ```sh # basic example -https://api-aniwatch.onrender.com/anime/search?q={query}&page={page} +/api/v2/hianime/search?q={query}&page={page} # advanced example -https://api-aniwatch.onrender.com/anime/search?q={query}&genres={genres}&type={type}&sort={sort}&season={season}&language={sub_or_dub}&status={status}&rated={rating}&start_date={yyyy-mm-dd}&end_date={yyyy-mm-dd}&score={score} +/api/v2/hianime/search?q={query}&page={page}&genres={genres}&type={type}&sort={sort}&season={season}&language={sub_or_dub}&status={status}&rated={rating}&start_date={yyyy-mm-dd}&end_date={yyyy-mm-dd}&score={score} ``` #### Query Parameters @@ -492,15 +514,13 @@ https://api-aniwatch.onrender.com/anime/search?q={query}&genres={genres}&type={t ```javascript // basic example -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/search?q=titan&page=1" -); +const resp = await fetch("/api/v2/hianime/search?q=titan&page=1"); const data = await resp.json(); console.log(data); // advanced example const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/search?q=girls&genres=action,adventure&type=movie&sort=score&season=spring&language=dub&status=finished-airing&rated=pg-13&start_date=2014-0-0&score=good" + "/api/v2/hianime/search?q=girls&genres=action,adventure&type=movie&sort=score&season=spring&language=dub&status=finished-airing&rated=pg-13&start_date=2014-0-0&score=good" ); const data = await resp.json(); console.log(data); @@ -510,42 +530,45 @@ console.log(data); ```javascript { - animes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - mostPopularAnimes: [ - { - episodes: { - sub: number, - dub: number, + success: true, + data: { + animes: [ + { + id: string, + name: string, + poster: string, + duration: string, + type: string, + rating: string, + episodes: { + sub: number, + dub: number, + } }, - id: string, - jname: string, - name: string, - poster: string, - type: string - }, - {...}, - ], - currentPage: 1, - totalPages: 1, - hasNextPage: false, - searchQuery: string, - searchFilters: { - [filter_name]: [filter_value] - ... + {...}, + ], + mostPopularAnimes: [ + { + episodes: { + sub: number, + dub: number, + }, + id: string, + jname: string, + name: string, + poster: string, + type: string + }, + {...}, + ], + currentPage: 1, + totalPages: 1, + hasNextPage: false, + searchQuery: string, + searchFilters: { + [filter_name]: [filter_value] + ... + } } } ``` @@ -565,7 +588,7 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/search/suggest?q={query} +/api/v2/hianime/search/suggestion?q={query} ``` #### Query Parameters @@ -577,9 +600,7 @@ https://api-aniwatch.onrender.com/anime/search/suggest?q={query} #### Request sample ```javascript -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/search/suggest?q=monster" -); +const resp = await fetch("/api/v2/hianime/search/suggestion?q=monster"); const data = await resp.json(); console.log(data); ``` @@ -588,16 +609,19 @@ console.log(data); ```javascript { - suggestions: [ - { - id: string, - name: string, - poster: string, - jname: string, - moreInfo: ["Jan 21, 2022", "Movie", "17m"] - }, - {...}, - ], + success: true, + data: { + suggestions: [ + { + id: string, + name: string, + poster: string, + jname: string, + moreInfo: ["Jan 21, 2022", "Movie", "17m"] + }, + {...}, + ] + } } ``` @@ -616,7 +640,7 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/producer/{name}?page={page} +/api/v2/hianime/producer/{name}?page={page} ``` #### Path Parameters @@ -634,9 +658,7 @@ https://api-aniwatch.onrender.com/anime/producer/{name}?page={page} #### Request sample ```javascript -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/producer/toei-animation?page=2" -); +const resp = await fetch("/api/v2/hianime/producer/toei-animation?page=2"); const data = await resp.json(); console.log(data); ``` @@ -645,56 +667,59 @@ console.log(data); ```javascript { - producerName: "Toei Animation Anime", - animes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } + success: true, + data: { + producerName: "Toei Animation Anime", + animes: [ + { + id: string, + name: string, + poster: string, + duration: string, + type: string, + rating: string, + episodes: { + sub: number, + dub: number, + } + }, + {...}, + ], + top10Animes: { + today: [ + { + episodes: { + sub: number, + dub: number, + }, + id: string, + name: string, + poster: string, + rank: number + }, + {...}, + ], + month: [...], + week: [...] }, - {...}, - ], - top10Animes: { - today: [ + topAiringAnimes: [ { episodes: { sub: number, dub: number, }, id: string, + jname: string, name: string, poster: string, - rank: number + type: string }, {...}, ], - month: [...], - week: [...] - }, - topAiringAnimes: [ - { - episodes: { - sub: number, - dub: number, - }, - id: string, - jname: string, - name: string, - poster: string, - type: string - }, - {...}, - ], - currentPage: 2, - totalPages: 11, - hasNextPage: true, + currentPage: 2, + totalPages: 11, + hasNextPage: true + } } ``` @@ -713,7 +738,7 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/genre/{name}?page={page} +/api/v2/hianime/genre/{name}?page={page} ``` #### Path Parameters @@ -731,9 +756,7 @@ https://api-aniwatch.onrender.com/anime/genre/{name}?page={page} #### Request sample ```javascript -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/genre/shounen?page=2" -); +const resp = await fetch("/api/v2/hianime/genre/shounen?page=2"); const data = await resp.json(); console.log(data); ``` @@ -742,40 +765,43 @@ console.log(data); ```javascript { - genreName: "Shounen Anime", - animes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - genres: ["Action", "Cars", "Adventure", ...], - topAiringAnimes: [ - { - episodes: { - sub: number, - dub: number, + success: true, + data: { + genreName: "Shounen Anime", + animes: [ + { + id: string, + name: string, + poster: string, + duration: string, + type: string, + rating: string, + episodes: { + sub: number, + dub: number, + } }, - id: string, - jname: string, - name: string, - poster: string, - type: string - }, - {...}, - ], - currentPage: 2, - totalPages: 38, - hasNextPage: true + {...}, + ], + genres: ["Action", "Cars", "Adventure", ...], + topAiringAnimes: [ + { + episodes: { + sub: number, + dub: number, + }, + id: string, + jname: string, + name: string, + poster: string, + type: string + }, + {...}, + ], + currentPage: 2, + totalPages: 38, + hasNextPage: true + } } ``` @@ -787,14 +813,14 @@ console.log(data); <summary> -### `GET` Category Anime +### `GET` Category Animes </summary> #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/{category}?page={page} +/api/v2/hianime/category/{name}?page={page} ``` #### Path Parameters @@ -814,7 +840,7 @@ https://api-aniwatch.onrender.com/anime/{category}?page={page} ```javascript // categories -> "most-favorite", "most-popular", "subbed-anime", "dubbed-anime", "recently-updated", "recently-added", "top-upcoming", "top-airing", "movie", "special", "ova", "ona", "tv", "completed" -const resp = await fetch("https://api-aniwatch.onrender.com/anime/tv?page=2"); +const resp = await fetch("/api/v2/hianime/category/tv?page=2"); const data = await resp.json(); console.log(data); ``` @@ -823,43 +849,46 @@ console.log(data); ```javascript { - category: "TV Series Anime", - animes: [ - { - id: string, - name: string, - poster: string, - duration: string, - type: string, - rating: string, - episodes: { - sub: number, - dub: number, - } - }, - {...}, - ], - genres: ["Action", "Cars", "Adventure", ...], - top10Animes: { - today: [ + success: true, + data: { + category: "TV Series Anime", + animes: [ { - episodes: { - sub: number, - dub: number, - }, id: string, name: string, poster: string, - rank: number + duration: string, + type: string, + rating: string, + episodes: { + sub: number, + dub: number, + } }, {...}, ], - month: [...], - week: [...] - }, - currentPage: 2, - totalPages: 100, - hasNextPage: true + genres: ["Action", "Cars", "Adventure", ...], + top10Animes: { + today: [ + { + episodes: { + sub: number, + dub: number, + }, + id: string, + name: string, + poster: string, + rank: number + }, + {...}, + ], + month: [...], + week: [...] + }, + currentPage: 2, + totalPages: 100, + hasNextPage: true + } } ``` @@ -878,7 +907,7 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/schedule?date={date} +/api/v2/hianime/schedule?date={date} ``` #### Query Parameters @@ -890,9 +919,7 @@ https://api-aniwatch.onrender.com/anime/schedule?date={date} #### Request sample ```javascript -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/schedule?date=2023-01-14" -); +const resp = await fetch("/api/v2/hianime/schedule?date=2024-06-09"); const data = await resp.json(); console.log(data); ``` @@ -901,17 +928,20 @@ console.log(data); ```javascript { - scheduledAnimes: [ - { - id: string, - time: string, // 24 hours format - name: string, - jname: string, - airingTimestamp: number, - secondsUntilAiring: number - }, - {...} - ] + success: true, + data: { + scheduledAnimes: [ + { + id: string, + time: string, // 24 hours format + name: string, + jname: string, + airingTimestamp: number, + secondsUntilAiring: number + }, + {...} + ] + } } ``` @@ -930,7 +960,7 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/episodes/{animeId} +/api/v2/hianime/anime/{animeId}/episodes ``` #### Path Parameters @@ -942,9 +972,7 @@ https://api-aniwatch.onrender.com/anime/episodes/{animeId} #### Request sample ```javascript -const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/episodes/steinsgate-3" -); +const resp = await fetch("/api/v2/hianime/anime/steinsgate-3/episodes"); const data = await resp.json(); console.log(data); ``` @@ -953,16 +981,19 @@ console.log(data); ```javascript { - totalEpisodes: 24, - episodes: [ - { - number: 1, - title: "Turning Point", - episodeId: "steinsgate-3?ep=213" - isFiller: false, - }, - {...} - ] + success: true, + data: { + totalEpisodes: 24, + episodes: [ + { + number: 1, + title: "Turning Point", + episodeId: "steinsgate-3?ep=213" + isFiller: false, + }, + {...} + ] + } } ``` @@ -981,20 +1012,20 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/servers?episodeId={id} +/api/v2/hianime/episode/servers?animeEpisodeId={id} ``` #### Query Parameters -| Parameter | Type | Description | Required? | Default | -| :---------: | :----: | :--------------------: | :-------: | :-----: | -| `episodeId` | string | The unique episode id. | Yes | -- | +| Parameter | Type | Description | Required? | Default | +| :--------------: | :----: | :--------------------------: | :-------: | :-----: | +| `animeEpisodeId` | string | The unique anime episode id. | Yes | -- | #### Request sample ```javascript const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/servers?episodeId=steinsgate-0-92?ep=2055" + "/api/v2/hianime/episode/servers?animeEpisodeId=steinsgate-0-92?ep=2055" ); const data = await resp.json(); console.log(data); @@ -1004,29 +1035,32 @@ console.log(data); ```javascript { - episodeId: "steinsgate-0-92?ep=2055", - episodeNo: 5, - sub: [ - { - serverId: 4, - serverName: "vidstreaming", - }, - {...} - ], - dub: [ - { - serverId: 1, - serverName: "megacloud", - }, - {...} - ], - raw: [ - { - serverId: 1, - serverName: "megacloud", - }, - {...} - ], + success: true, + data: { + episodeId: "steinsgate-0-92?ep=2055", + episodeNo: 5, + sub: [ + { + serverId: 4, + serverName: "vidstreaming", + }, + {...} + ], + dub: [ + { + serverId: 1, + serverName: "megacloud", + }, + {...} + ], + raw: [ + { + serverId: 1, + serverName: "megacloud", + }, + {...} + ] + } } ``` @@ -1045,22 +1079,22 @@ console.log(data); #### Endpoint ```sh -https://api-aniwatch.onrender.com/anime/episode-srcs?id={episodeId}&server={server}&category={category} +/api/v2/hianime/episode/sources?animeEpisodeId={id}?server={server}&category={dub || sub || raw} ``` #### Query Parameters -| Parameter | Type | Description | Required? | Default | -| :--------: | :----: | :--------------------------------------------------: | :-------: | :--------------: | -| `id` | string | The id of the episode. | Yes | -- | -| `server` | string | The name of the server. | No | `"hd-1"` | -| `category` | string | The category of the episode ('sub', 'dub' or 'raw'). | No | `"sub"` | +| Parameter | Type | Description | Required? | Default | +| :--------------: | :----: | :--------------------------------------------------: | :-------: | :------: | +| `animeEpisodeId` | string | The unique anime episode id. | Yes | -- | +| `server` | string | The name of the server. | No | `"hd-1"` | +| `category` | string | The category of the episode ('sub', 'dub' or 'raw'). | No | `"sub"` | #### Request sample ```javascript const resp = await fetch( - "https://api-aniwatch.onrender.com/anime/episode-srcs?id=steinsgate-3?ep=230&server=hd-1&category=dub" + "/api/v2/hianime/episode/sources?animeEpisodeId=steinsgate-3?ep=230&server=hd-1&category=dub" ); const data = await resp.json(); console.log(data); @@ -1070,28 +1104,31 @@ console.log(data); ```javascript { - headers: { - Referer: string, - "User-Agent": string, - ... - }, - sources: [ - { - url: string, // .m3u8 hls streaming file - isM3U8: boolean, - quality?: string, - }, - {...} - ], - subtitles: [ - { - lang: "English", - url: string, // .vtt subtitle file + success: true, + data: { + headers: { + Referer: string, + "User-Agent": string, + ... }, - {...} - ], - anilistID: number | null, - malID: number | null, + sources: [ + { + url: string, // .m3u8 hls streaming file + isM3U8: boolean, + quality?: string, + }, + {...} + ], + subtitles: [ + { + lang: "English", + url: string, // .vtt subtitle file + }, + {...} + ], + anilistID: number | null, + malID: number | null + } } ``` diff --git a/api/index.ts b/api/index.ts index be3392b..15976e3 100644 --- a/api/index.ts +++ b/api/index.ts @@ -1,3 +1,10 @@ import app from "../src/server.js"; +import { handle } from "hono/vercel"; -export default app; +const handler = handle(app); + +export const GET = handler; +export const POST = handler; +export const PATCH = handler; +export const PUT = handler; +export const OPTIONS = handler; diff --git a/package.json b/package.json index 354b305..bbb970e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "aniwatch-api", "version": "1.40.0", - "description": "Node.js API for obtaining anime information from hianime.to (formerly aniwatch.to)", + "description": "Node.js API for obtaining anime information from hianime.to", "main": "src/server.ts", "type": "module", "scripts": { @@ -31,26 +31,17 @@ "author": "https://github.com/ghoshRitesh12", "license": "MIT", "dependencies": { - "axios": "^1.7.7", - "cheerio": "1.0.0", - "cors": "^2.8.5", - "crypto-js": "^4.2.0", + "@hono/node-server": "^1.13.1", + "aniwatch": "^2.4.1", "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "^7.4.0", - "http-errors": "^2.0.0", - "morgan": "^1.10.0" + "hono": "^4.6.3", + "hono-rate-limiter": "^0.4.0" }, "devDependencies": { - "@types/cors": "^2.8.17", - "@types/crypto-js": "^4.2.2", - "@types/express": "^4.17.21", - "@types/http-errors": "^2.0.4", - "@types/morgan": "^1.9.9", - "@types/node": "^22.5.4", + "@types/node": "^22.7.4", "husky": "^8.0.3", - "tsx": "^4.19.0", - "typescript": "^5.5.4", - "vitest": "^2.0.5" + "tsx": "^4.19.1", + "typescript": "^5.6.2", + "vitest": "^2.1.2" } }
\ No newline at end of file diff --git a/public/img/dot.png b/public/img/dot.png Binary files differnew file mode 100644 index 0000000..d423036 --- /dev/null +++ b/public/img/dot.png diff --git a/public/img/wano_kuni.png b/public/img/wano_kuni.png Binary files differindex 4b1dfab..7659c5d 100644 --- a/public/img/wano_kuni.png +++ b/public/img/wano_kuni.png diff --git a/public/index.html b/public/index.html index d2c2c80..ff7384f 100644 --- a/public/index.html +++ b/public/index.html @@ -5,67 +5,151 @@ <meta charset="utf-8"> <title>Aniwatch API</title> <meta name="content-language" content="en"> - <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"> + <!-- <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"> --> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="title" content="Aniwatch API"> + <meta name="theme-color" content="#d5b3ff"> + <meta name="color-scheme" content="dark"> <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 property="og:image" - content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif"> + 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:image:width" content="457"> - <meta property="og:image:height" content="480"> + <meta property="og:image:width" content="500"> + <meta property="og:image:height" content="500"> <meta property="og:site_name" content="aniwatch"> <meta property="og:type" content="website"> - <meta name="twitter:card" content="summary_large_image"> + <meta name="twitter:card" content="summary_image"> <meta name="twitter:site" content="@aniwatch-api"> <meta name="twitter:title" content="Aniwatch API"> - <meta name="twitter:description" - content="Node Express API for obtaining anime information from hianime.to (formerly aniwatch.to)"> + <meta name="twitter:description" content="Node.js API for obtaining anime information from hianime.to"> <meta name="twitter:image:src" - content="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif"> + 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 property="og:title" content="Aniwatch API"> - <meta name="description" - content="Node Express API for obtaining anime information from hianime.to (formerly aniwatch.to)"> - <meta property="og:description" - content="Node Express API for obtaining anime information from hianime.to (formerly aniwatch.to)"> + <meta name="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 hianime.to"> <link rel="shortcut icon" - href="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif"> + href="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/hianime_v2.png"> <style> * { + --accent: #d5b3ff; box-sizing: border-box; font-family: sans-serif; + transition: + 0.15s ease scale, + 0.15s ease width, + 0.15s ease height, + 0.15s ease gap; } html { color-scheme: dark; + height: 100%; } body { display: flex; - align-items: center; - justify-content: start; flex-direction: column; font-family: sans-serif; - background: #18161A; - padding: 10rem 1rem 0 1rem; + background-color: #151518; + padding: 0rem 1rem 0 1rem; + min-height: 98vh; + } + + section { + margin-block: auto; + display: flex; + flex-direction: column; + align-items: center; + justify-content: start; + position: relative; + isolation: isolate; + } + + section::before { + content: ""; + width: 100%; + height: 100%; + + position: absolute; + inset: -2rem 0 0 0; + isolation: isolate; + z-index: -1; + + background-color: #7300ff; + opacity: .35; + border-radius: 50%; + filter: blur(12rem); + } + + @media (min-width: 640px) { + section::before { + background-color: #7300ffbb; + opacity: .3; + } + } + + .overlay { + position: fixed; + top: 0; + 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; + filter: blur(5rem); + background-image: url(https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/refs/heads/main/public/img/wano_kuni.png); + } + + @media (min-width: 640px) { + .overlay .child { + opacity: .35; + filter: blur(8rem); + } + } + + .overlay::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 2; + + background: url("https://hianime.to/images/live-thumb.png") repeat; } h2 { - background-clip: text; - background: linear-gradient(to left, #ff5a7e 40%, #00aeff); - text-align: center; + color: var(--accent); + margin-top: 0; + font-size: 1.65rem; font-weight: 600; - color: transparent; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + text-align: center; + text-wrap: balance; } a { - color: #00AEDD; + line-height: 1.3; + text-wrap: balance; + color: var(--accent); } img { @@ -74,13 +158,24 @@ font-size: .9rem; } + #anime_girl { + width: 100%; + max-width: 10rem; + } + + @media (min-width: 640px) { + #anime_girl { + max-width: 12rem; + } + } + .img-wrapper { aspect-ratio: 1/1; max-width: 12rem; border-radius: 1rem; overflow: hidden; width: fit-content; - margin: 1rem auto 1rem auto; + margin: 1rem auto 1.5rem auto; } .api-stats-container { @@ -104,62 +199,79 @@ </style> </head> -<!-- +<!-- IMPORTANT: -1. https://api-aniwatch.onrender.com is only meant to demo the API and has 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. -2. This API is just an unofficial api for hianime.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. +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. +2. This API is just an unofficial API for hianime.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. --> <body> - <h2> - Welcome to the unofficial - <a href="https://hianime.to" style="text-decoration: underline;">hianime.to (formerly aniwatch.to)</a> - api - <span style="-webkit-text-fill-color: white">⚔️</span> - </h2> - - <div class="api-stats-container"> - <a href="https://github.com/ghoshRitesh12/aniwatch-api/stargazers"> - <img src="https://img.shields.io/github/stars/ghoshRitesh12/aniwatch-api?style=social" alt="Stars" - decoding="async"> - </a> - <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" - decoding="async"> - </a> - <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" - decoding="async"> - </a> - <a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest"> - <img - src="https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch-api?display_name=release&style=social&logo=github" - alt="Version" decoding="async"> - </a> - </div> - - <div style="font-size: 1.2rem; text-align: center;"> - <div class="img-wrapper"> - <img draggable="false" src="https://raw.githubusercontent.com/ghoshRitesh12/aniwatch-api/main/public/img/img1.gif" - alt="kawai anime girl" decoding="async" fetchpriority="high" style="user-select: none;" /> + <span class="overlay"> + <span class="child"></span> + </span> + + <section> + <h2> + Welcome to the unofficial + <a href="https://hianime.to/home" style="text-underline-offset: 3px;">hianime.to</a> + API + <span style="-webkit-text-fill-color: var(--accent)">⚔️</span> + </h2> + + <div class="api-stats-container"> + <a href="https://github.com/ghoshRitesh12/aniwatch-api/stargazers"> + <img src="https://img.shields.io/github/stars/ghoshRitesh12/aniwatch-api?style=social" alt="Stars" + decoding="async"> + </a> + <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" + decoding="async"> + </a> + <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" + decoding="async"> + </a> + <a href="https://github.com/ghoshRitesh12/aniwatch-api/releases/latest"> + <img + src="https://img.shields.io/github/v/release/ghoshRitesh12/aniwatch-api?display_name=release&style=social&logo=github" + alt="Version" decoding="async"> + </a> </div> - <a href="https://github.com/ghoshRitesh12/aniwatch-api#documentation" rel="noopener noreferer"> - Visit docs for more information - </a> - </div> + <div style="text-align: center;"> + <div class="img-wrapper"> + <img draggable="false" id="anime_girl" + 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;" /> + </div> - <div class="contributors"> - <p>Thanks to the following people for keeping this project alive and relevant.</p> + <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"> + Checkout latest changelogs + </a> - <a href="https://github.com/ghoshRitesh12/aniwatch-api/graphs/contributors" - style="display: block; margin-bottom: 1rem; user-select: none;"> - <img src="https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch-api" alt="Contributors" decoding="async"> - </a> - </div> + <span style="font-size: .9rem;">•</span> + + <a href="https://github.com/ghoshRitesh12/aniwatch-api#documentation" rel="noopener noreferer"> + Visit docs for more information + </a> + </div> + </div> + + <div class="contributors"> + <p style="text-wrap: balance; line-height: 1.2;">Thanks to the following people for keeping this + project alive and + relevant.</p> + + <a href="https://github.com/ghoshRitesh12/aniwatch-api/graphs/contributors" + style="display: block; margin-bottom: 1rem; user-select: none;"> + <img src="https://contrib.rocks/image?repo=ghoshRitesh12/aniwatch-api" alt="Contributors" decoding="async"> + </a> + </div> + </section> </body> </html>
\ No newline at end of file diff --git a/src/config/axios.ts b/src/config/axios.ts deleted file mode 100644 index 6292e9f..0000000 --- a/src/config/axios.ts +++ /dev/null @@ -1,21 +0,0 @@ -import axios, { AxiosError, type AxiosRequestConfig } from "axios"; -import { - SRC_BASE_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/constants.js"; - -const clientConfig: AxiosRequestConfig = { - timeout: 10000, - baseURL: SRC_BASE_URL, - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, -}; - -const client = axios.create(clientConfig); - -export { client, AxiosError }; diff --git a/src/config/cors.ts b/src/config/cors.ts index 8089522..6d6e138 100644 --- a/src/config/cors.ts +++ b/src/config/cors.ts @@ -1,24 +1,17 @@ -import cors from "cors"; import { config } from "dotenv"; +import { cors } from "hono/cors"; config(); -const allowedOrigins = process.env.CORS_ALLOWED_ORIGINS - ? process.env.CORS_ALLOWED_ORIGINS.split(",") +const allowedOrigins = process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS + ? process.env.ANIWATCH_API_CORS_ALLOWED_ORIGINS.split(",") : ["http://localhost:4000", "*"]; const corsConfig = cors({ - origin: function (origin, callback) { - if (!origin || allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(new Error("Not allowed by CORS")); - } - }, - methods: ["GET"], + allowMethods: ["GET"], maxAge: 600, credentials: true, - optionsSuccessStatus: 200, + origin: allowedOrigins, }); export default corsConfig; diff --git a/src/config/errorHandler.ts b/src/config/errorHandler.ts deleted file mode 100644 index 560f141..0000000 --- a/src/config/errorHandler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ErrorRequestHandler } from "express"; - -const errorHandler: ErrorRequestHandler = (error, req, res, next) => { - const status = error?.status || 500; - res.status(status).json({ - status, - message: error?.message || "Something Went Wrong", - }); -}; - -export default errorHandler; diff --git a/src/config/notFoundHandler.ts b/src/config/notFoundHandler.ts deleted file mode 100644 index a372e8e..0000000 --- a/src/config/notFoundHandler.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { RequestHandler } from "express"; -import createHttpError from "http-errors"; - -const notFoundHandler: RequestHandler = (req, res, next) => { - return next(createHttpError.NotFound()); -}; - -export default notFoundHandler; diff --git a/src/config/ratelimit.ts b/src/config/ratelimit.ts index c5add3f..afd6e54 100644 --- a/src/config/ratelimit.ts +++ b/src/config/ratelimit.ts @@ -1,17 +1,21 @@ import { config } from "dotenv"; -import createHttpError from "http-errors"; -import { rateLimit } from "express-rate-limit"; +import { rateLimiter } from "hono-rate-limiter"; +import { getConnInfo } from "@hono/node-server/conninfo"; config(); -export const ratelimit = rateLimit({ - windowMs: Number(process.env.WINDOWMS) || 30 * 60 * 1000, - limit: Number(process.env.MAX) || 6, - legacyHeaders: true, +export const ratelimit = rateLimiter({ + windowMs: Number(process.env.ANIWATCH_API_WINDOW_MS) || 30 * 60 * 1000, + limit: Number(process.env.ANIWATCH_API_MAX_REQS) || 6, standardHeaders: "draft-7", - handler: function (_, __, next) { - next( - createHttpError.TooManyRequests("Too many API requests, try again later") - ); + keyGenerator(c) { + const { remote } = getConnInfo(c); + const key = + `${String(remote.addressType)}_` + + `${String(remote.address)}:${String(remote.port)}`; + + return key; }, + handler: (c) => + c.json({ status: 429, message: "Too Many Requests 😵" }, { status: 429 }), }); diff --git a/src/controllers/animeAboutInfo.controller.ts b/src/controllers/animeAboutInfo.controller.ts deleted file mode 100644 index 6d50b50..0000000 --- a/src/controllers/animeAboutInfo.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeAboutInfo } from "../parsers/index.js"; -import { type AnimeAboutInfoQueryParams } from "../types/controllers/index.js"; - -// /anime/info?id=${anime-id} -const getAnimeAboutInfo: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeAnimeAboutInfo>>, - unknown, - AnimeAboutInfoQueryParams -> = async (req, res, next) => { - try { - const animeId = req.query.id - ? decodeURIComponent(req.query.id as string) - : null; - - if (animeId === null) { - throw createHttpError.BadRequest("Anime unique id required"); - } - - const data = await scrapeAnimeAboutInfo(animeId); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeAboutInfo; diff --git a/src/controllers/animeCategory.controller.ts b/src/controllers/animeCategory.controller.ts deleted file mode 100644 index 373b8e3..0000000 --- a/src/controllers/animeCategory.controller.ts +++ /dev/null @@ -1,39 +0,0 @@ -import createHttpError from "http-errors"; -import type { RequestHandler } from "express"; -import type { AnimeCategories } from "../types/anime.js"; -import { scrapeAnimeCategory } from "../parsers/index.js"; -import type { - CategoryAnimePathParams, - CategoryAnimeQueryParams, -} from "../types/controllers/index.js"; - -// /anime/:category?page=${page} -const getAnimeCategory: RequestHandler< - CategoryAnimePathParams, - Awaited<ReturnType<typeof scrapeAnimeCategory>>, - unknown, - CategoryAnimeQueryParams -> = async (req, res, next) => { - try { - const category = req.params.category - ? decodeURIComponent(req.params.category) - : null; - - const page: number = req.query.page - ? Number(decodeURIComponent(req.query?.page as string)) - : 1; - - if (category === null) { - throw createHttpError.BadRequest("category required"); - } - - const data = await scrapeAnimeCategory(category as AnimeCategories, page); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeCategory; diff --git a/src/controllers/animeEpisodeSrcs.controller.ts b/src/controllers/animeEpisodeSrcs.controller.ts deleted file mode 100644 index 6190ce5..0000000 --- a/src/controllers/animeEpisodeSrcs.controller.ts +++ /dev/null @@ -1,75 +0,0 @@ -import axios from "axios"; -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { type CheerioAPI, load } from "cheerio"; -import { scrapeAnimeEpisodeSources } from "../parsers/index.js"; -import { USER_AGENT_HEADER, SRC_BASE_URL } from "../utils/constants.js"; -import { type AnimeServers, Servers } from "../types/anime.js"; -import { type AnimeEpisodeSrcsQueryParams } from "../types/controllers/index.js"; - -type AnilistID = number | null; -type MalID = number | null; - -// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} -const getAnimeEpisodeSources: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeAnimeEpisodeSources & AnilistID>>, - unknown, - AnimeEpisodeSrcsQueryParams -> = async (req, res, next) => { - try { - const episodeId = req.query.id ? decodeURIComponent(req.query.id) : null; - - const server = ( - req.query.server - ? decodeURIComponent(req.query.server) - : Servers.VidStreaming - ) as AnimeServers; - - const category = ( - req.query.category ? decodeURIComponent(req.query.category) : "sub" - ) as "sub" | "dub"; - - if (episodeId === null) { - throw createHttpError.BadRequest("Anime episode id required"); - } - - let malID: MalID; - let anilistID: AnilistID; - const animeURL = new URL(episodeId?.split("?ep=")[0], SRC_BASE_URL)?.href; - - const [episodeSrcData, animeSrc] = await Promise.all([ - scrapeAnimeEpisodeSources(episodeId, server, category), - axios.get(animeURL, { - headers: { - Referer: SRC_BASE_URL, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - }, - }), - ]); - - const $: CheerioAPI = load(animeSrc?.data); - - try { - anilistID = Number( - JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id - ); - malID = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id); - } catch (err) { - anilistID = null; - malID = null; - } - - res.status(200).json({ - ...episodeSrcData, - anilistID, - malID, - }); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeEpisodeSources; diff --git a/src/controllers/animeEpisodes.controller.ts b/src/controllers/animeEpisodes.controller.ts deleted file mode 100644 index cf815f5..0000000 --- a/src/controllers/animeEpisodes.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeEpisodes } from "../parsers/index.js"; -import { type AnimeEpisodePathParams } from "../types/controllers/index.js"; - -// /anime/episodes/${anime-id} -const getAnimeEpisodes: RequestHandler< - AnimeEpisodePathParams, - Awaited<ReturnType<typeof scrapeAnimeEpisodes>>, - unknown, - unknown -> = async (req, res, next) => { - try { - const animeId = req.params.animeId - ? decodeURIComponent(req.params.animeId) - : null; - - if (animeId === null) { - throw createHttpError.BadRequest("Anime Id required"); - } - - const data = await scrapeAnimeEpisodes(animeId); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeEpisodes; diff --git a/src/controllers/animeGenre.controller.ts b/src/controllers/animeGenre.controller.ts deleted file mode 100644 index 486b4c7..0000000 --- a/src/controllers/animeGenre.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeGenreAnime } from "../parsers/index.js"; -import type { - GenreAnimePathParams, - GenreAnimeQueryParams, -} from "../types/controllers/index.js"; - -// /anime/genre/${name}?page=${page} -const getGenreAnime: RequestHandler< - GenreAnimePathParams, - Awaited<ReturnType<typeof scrapeGenreAnime>>, - unknown, - GenreAnimeQueryParams -> = async (req, res, next) => { - try { - const name: string | null = req.params.name - ? decodeURIComponent(req.params.name as string) - : null; - - const page: number = req.query.page - ? Number(decodeURIComponent(req.query?.page as string)) - : 1; - - if (name === null) { - throw createHttpError.BadRequest("Anime genre required"); - } - - const data = await scrapeGenreAnime(name, page); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getGenreAnime; diff --git a/src/controllers/animeProducer.controller.ts b/src/controllers/animeProducer.controller.ts deleted file mode 100644 index 3ebcd8a..0000000 --- a/src/controllers/animeProducer.controller.ts +++ /dev/null @@ -1,37 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeProducerAnimes } from "../parsers/index.js"; -import type { - AnimeProducerPathParams, - AnimeProducerQueryParams, -} from "../types/controllers/index.js"; - -// /anime/producer/${name}?page=${page} -const getProducerAnimes: RequestHandler< - AnimeProducerPathParams, - Awaited<ReturnType<typeof scrapeProducerAnimes>>, - unknown, - AnimeProducerQueryParams -> = async (req, res, next) => { - try { - const name: string | null = req.params.name - ? decodeURIComponent(req.params.name as string) - : null; - - const page: number = req.query.page - ? Number(decodeURIComponent(req.query?.page as string)) - : 1; - - if (name === null) { - throw createHttpError.BadRequest("Anime producer name required"); - } - - const data = await scrapeProducerAnimes(name, page); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getProducerAnimes; diff --git a/src/controllers/animeSearch.controller.ts b/src/controllers/animeSearch.controller.ts deleted file mode 100644 index 8937b0a..0000000 --- a/src/controllers/animeSearch.controller.ts +++ /dev/null @@ -1,57 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeSearch } from "../parsers/index.js"; -import type { - SearchFilters, - AnimeSearchQueryParams, -} from "../types/controllers/index.js"; - -const searchFilters: Record<string, boolean> = { - filter: true, - type: true, - status: true, - rated: true, - score: true, - season: true, - language: true, - start_date: true, - end_date: true, - sort: true, - genres: true, -} as const; - -// /anime/search?q=${query}&page=${page} -const getAnimeSearch: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeAnimeSearch>>, - unknown, - AnimeSearchQueryParams -> = async (req, res, next) => { - try { - let { q: query, page, ...filters } = req.query; - - query = query ? decodeURIComponent(query) : undefined; - const pageNo = page ? Number(decodeURIComponent(page as string)) : 1; - - if (query === undefined) { - throw createHttpError.BadRequest("Search keyword required"); - } - - const parsedFilters: SearchFilters = {}; - for (const key in filters) { - if (searchFilters[key]) { - parsedFilters[key as keyof SearchFilters] = - filters[key as keyof SearchFilters]; - } - } - - const data = await scrapeAnimeSearch(query, pageNo, parsedFilters); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeSearch; diff --git a/src/controllers/animeSearchSuggestion.controller.ts b/src/controllers/animeSearchSuggestion.controller.ts deleted file mode 100644 index ed8784f..0000000 --- a/src/controllers/animeSearchSuggestion.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeAnimeSearchSuggestion } from "../parsers/index.js"; -import { type AnimeSearchSuggestQueryParams } from "../types/controllers/index.js"; - -// /anime/search/suggest?q=${query} -const getAnimeSearchSuggestion: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeAnimeSearchSuggestion>>, - unknown, - AnimeSearchSuggestQueryParams -> = async (req, res, next) => { - try { - const query: string | null = req.query.q - ? decodeURIComponent(req.query.q as string) - : null; - - if (query === null) { - throw createHttpError.BadRequest("Search keyword required"); - } - - const data = await scrapeAnimeSearchSuggestion(query); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getAnimeSearchSuggestion; diff --git a/src/controllers/episodeServers.controller.ts b/src/controllers/episodeServers.controller.ts deleted file mode 100644 index 16e1ca5..0000000 --- a/src/controllers/episodeServers.controller.ts +++ /dev/null @@ -1,30 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeEpisodeServers } from "../parsers/index.js"; -import { type EpisodeServersQueryParams } from "../types/controllers/index.js"; - -// /anime/servers?episodeId=${id} -const getEpisodeServers: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeEpisodeServers>>, - unknown, - EpisodeServersQueryParams -> = async (req, res, next) => { - try { - const episodeId = req.query.episodeId - ? decodeURIComponent(req.query?.episodeId as string) - : null; - - if (episodeId === null) { - throw createHttpError.BadRequest("Episode id required"); - } - - const data = await scrapeEpisodeServers(episodeId); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getEpisodeServers; diff --git a/src/controllers/estimatedSchedule.controller.ts b/src/controllers/estimatedSchedule.controller.ts deleted file mode 100644 index fef2516..0000000 --- a/src/controllers/estimatedSchedule.controller.ts +++ /dev/null @@ -1,36 +0,0 @@ -import createHttpError from "http-errors"; -import { type RequestHandler } from "express"; -import { scrapeEstimatedSchedule } from "../parsers/index.js"; -import { type EstimatedScheduleQueryParams } from "../types/controllers/index.js"; - -// /anime/schedule?date=${date} -const getEstimatedSchedule: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeEstimatedSchedule>>, - unknown, - EstimatedScheduleQueryParams -> = async (req, res, next) => { - try { - const dateQuery = req.query.date - ? decodeURIComponent(req.query.date as string) - : null; - - if (dateQuery === null) { - throw createHttpError.BadRequest("Date payload required"); - } - if (!/^\d{4}-\d{2}-\d{2}$/.test(dateQuery)) { - throw createHttpError.BadRequest( - "Invalid date payload format. Months and days must have 2 digits" - ); - } - - const data = await scrapeEstimatedSchedule(dateQuery); - - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getEstimatedSchedule; diff --git a/src/controllers/homePage.controller.ts b/src/controllers/homePage.controller.ts deleted file mode 100644 index 36e1fe9..0000000 --- a/src/controllers/homePage.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type RequestHandler } from "express"; -import { scrapeHomePage } from "../parsers/index.js"; - -// /anime/home -const getHomePageInfo: RequestHandler< - unknown, - Awaited<ReturnType<typeof scrapeHomePage>> -> = async (req, res, next) => { - try { - const data = await scrapeHomePage(); - res.status(200).json(data); - } catch (err: any) { - console.error(err); - next(err); - } -}; - -export default getHomePageInfo; diff --git a/src/controllers/index.ts b/src/controllers/index.ts deleted file mode 100644 index 6e90440..0000000 --- a/src/controllers/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import getGenreAnime from "./animeGenre.controller.js"; -import getHomePageInfo from "./homePage.controller.js"; -import getAnimeSearch from "./animeSearch.controller.js"; -import getAnimeEpisodes from "./animeEpisodes.controller.js"; -import getAnimeCategory from "./animeCategory.controller.js"; -import getProducerAnimes from "./animeProducer.controller.js"; -import getEpisodeServers from "./episodeServers.controller.js"; -import getAnimeAboutInfo from "./animeAboutInfo.controller.js"; -import getEstimatedSchedule from "./estimatedSchedule.controller.js"; -import getAnimeEpisodeSources from "./animeEpisodeSrcs.controller.js"; -import getAnimeSearchSuggestion from "./animeSearchSuggestion.controller.js"; - -export { - getGenreAnime, - getAnimeSearch, - getHomePageInfo, - getAnimeEpisodes, - getAnimeCategory, - getEpisodeServers, - getProducerAnimes, - getAnimeAboutInfo, - getEstimatedSchedule, - getAnimeEpisodeSources, - getAnimeSearchSuggestion, -}; diff --git a/src/extractors/index.ts b/src/extractors/index.ts deleted file mode 100644 index 788dc8c..0000000 --- a/src/extractors/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import StreamSB from "./streamsb.js"; -import StreamTape from "./streamtape.js"; -import RapidCloud from "./rapidcloud.js"; -import MegaCloud from "./megacloud.js"; - -export { StreamSB, StreamTape, RapidCloud, MegaCloud }; diff --git a/src/extractors/megacloud.ts b/src/extractors/megacloud.ts deleted file mode 100644 index bbed720..0000000 --- a/src/extractors/megacloud.ts +++ /dev/null @@ -1,226 +0,0 @@ -import axios from "axios"; -import crypto from "crypto"; -import createHttpError from "http-errors"; - -// https://megacloud.tv/embed-2/e-1/dBqCr5BcOhnD?k=1 - -const megacloud = { - script: "https://megacloud.tv/js/player/a/prod/e1-player.min.js?v=", - sources: "https://megacloud.tv/embed-2/ajax/e-1/getSources?id=", -} as const; - -type track = { - file: string; - kind: string; - label?: string; - default?: boolean; -}; - -type intro_outro = { - start: number; - end: number; -}; - -type unencryptedSrc = { - file: string; - type: string; -}; - -type extractedSrc = { - sources: string | unencryptedSrc[]; - tracks: track[]; - encrypted: boolean; - intro: intro_outro; - outro: intro_outro; - server: number; -}; - -interface ExtractedData - extends Pick<extractedSrc, "intro" | "outro" | "tracks"> { - sources: { url: string; type: string }[]; -} - -class MegaCloud { - private serverName = "megacloud"; - - async extract(videoUrl: URL) { - try { - const extractedData: ExtractedData = { - tracks: [], - intro: { - start: 0, - end: 0, - }, - outro: { - start: 0, - end: 0, - }, - sources: [], - }; - - const videoId = videoUrl?.href?.split("/")?.pop()?.split("?")[0]; - const { data: srcsData } = await axios.get<extractedSrc>( - megacloud.sources.concat(videoId || ""), - { - headers: { - Accept: "*/*", - "X-Requested-With": "XMLHttpRequest", - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", - Referer: videoUrl.href, - }, - } - ); - if (!srcsData) { - throw createHttpError.NotFound("Url may have an invalid video id"); - } - - // console.log(JSON.stringify(srcsData, null, 2)); - - const encryptedString = srcsData.sources; - if (!srcsData.encrypted && Array.isArray(encryptedString)) { - extractedData.intro = srcsData.intro; - extractedData.outro = srcsData.outro; - extractedData.tracks = srcsData.tracks; - extractedData.sources = encryptedString.map((s) => ({ - url: s.file, - type: s.type, - })); - - return extractedData; - } - - let text: string; - const { data } = await axios.get( - megacloud.script.concat(Date.now().toString()) - ); - - text = data; - if (!text) { - throw createHttpError.InternalServerError( - "Couldn't fetch script to decrypt resource" - ); - } - - const vars = this.extractVariables(text); - if (!vars.length) { - throw new Error("Can't find variables. Perhaps the extractor is outdated."); - } - - const { secret, encryptedSource } = this.getSecret( - encryptedString as string, - vars - ); - const decrypted = this.decrypt(encryptedSource, secret); - try { - const sources = JSON.parse(decrypted); - extractedData.intro = srcsData.intro; - extractedData.outro = srcsData.outro; - extractedData.tracks = srcsData.tracks; - extractedData.sources = sources.map((s: any) => ({ - url: s.file, - type: s.type, - })); - - return extractedData; - } catch (error) { - throw createHttpError.InternalServerError("Failed to decrypt resource"); - } - } catch (err) { - // console.log(err); - throw err; - } - } - - extractVariables(text: string) { - // copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' - const regex = - /case\s*0x[0-9a-f]+:(?![^;]*=partKey)\s*\w+\s*=\s*(\w+)\s*,\s*\w+\s*=\s*(\w+);/g; - const matches = text.matchAll(regex); - const vars = Array.from(matches, (match) => { - const matchKey1 = this.matchingKey(match[1], text); - const matchKey2 = this.matchingKey(match[2], text); - try { - return [parseInt(matchKey1, 16), parseInt(matchKey2, 16)]; - } catch (e) { - return []; - } - }).filter((pair) => pair.length > 0); - - return vars; - } - - getSecret(encryptedString: string, values: number[][]) { - let secret = "", - encryptedSource = "", - encryptedSourceArray = encryptedString.split(""), - currentIndex = 0; - - for (const index of values) { - const start = index[0] + currentIndex; - const end = start + index[1]; - - for (let i = start; i < end; i++) { - secret += encryptedString[i]; - encryptedSourceArray[i] = ""; - } - currentIndex += index[1]; - } - - encryptedSource = encryptedSourceArray.join(""); - - return { secret, encryptedSource }; - } - - decrypt(encrypted: string, keyOrSecret: string, maybe_iv?: string) { - let key; - let iv; - let contents; - if (maybe_iv) { - key = keyOrSecret; - iv = maybe_iv; - contents = encrypted; - } else { - // copied from 'https://github.com/brix/crypto-js/issues/468' - const cypher = Buffer.from(encrypted, "base64"); - const salt = cypher.subarray(8, 16); - const password = Buffer.concat([ - Buffer.from(keyOrSecret, "binary"), - salt, - ]); - const md5Hashes = []; - let digest = password; - for (let i = 0; i < 3; i++) { - md5Hashes[i] = crypto.createHash("md5").update(digest).digest(); - digest = Buffer.concat([md5Hashes[i], password]); - } - key = Buffer.concat([md5Hashes[0], md5Hashes[1]]); - iv = md5Hashes[2]; - contents = cypher.subarray(16); - } - - const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); - const decrypted = - decipher.update( - contents as any, - typeof contents === "string" ? "base64" : undefined, - "utf8" - ) + decipher.final(); - - return decrypted; - } - - // function copied from github issue #30 'https://github.com/ghoshRitesh12/aniwatch-api/issues/30' - matchingKey(value: string, script: string) { - const regex = new RegExp(`,${value}=((?:0x)?([0-9a-fA-F]+))`); - const match = script.match(regex); - if (match) { - return match[1].replace(/^0x/, ""); - } else { - throw new Error("Failed to match the key"); - } - } - -} - -export default MegaCloud; diff --git a/src/extractors/rapidcloud.ts b/src/extractors/rapidcloud.ts deleted file mode 100644 index 8073c8b..0000000 --- a/src/extractors/rapidcloud.ts +++ /dev/null @@ -1,166 +0,0 @@ -import axios from "axios"; -import CryptoJS from "crypto-js"; -import { substringAfter, substringBefore } from "../utils/index.js"; -import type { Video, Subtitle, Intro } from "../types/extractor.js"; - -type extractReturn = { - sources: Video[]; - subtitles: Subtitle[]; -}; - -// https://megacloud.tv/embed-2/e-1/IxJ7GjGVCyml?k=1 -class RapidCloud { - private serverName = "RapidCloud"; - private sources: Video[] = []; - - // https://rapid-cloud.co/embed-6/eVZPDXwVfrY3?vast=1 - private readonly fallbackKey = "c1d17096f2ca11b7"; - private readonly host = "https://rapid-cloud.co"; - - async extract(videoUrl: URL): Promise<extractReturn> { - const result: extractReturn & { intro?: Intro; outro?: Intro } = { - sources: [], - subtitles: [], - }; - - try { - const id = videoUrl.href.split("/").pop()?.split("?")[0]; - const options = { - headers: { - "X-Requested-With": "XMLHttpRequest", - }, - }; - - let res = null; - - res = await axios.get( - `https://${videoUrl.hostname}/embed-2/ajax/e-1/getSources?id=${id}`, - options - ); - - let { - data: { sources, tracks, intro, outro, encrypted }, - } = res; - - let decryptKey = await ( - await axios.get( - "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key" - ) - ).data; - - decryptKey = substringBefore( - substringAfter(decryptKey, '"blob-code blob-code-inner js-file-line">'), - "</td>" - ); - - if (!decryptKey) { - decryptKey = await ( - await axios.get( - "https://raw.githubusercontent.com/cinemaxhq/keys/e1/key" - ) - ).data; - } - - if (!decryptKey) decryptKey = this.fallbackKey; - - try { - if (encrypted) { - const sourcesArray = sources.split(""); - let extractedKey = ""; - let currentIndex = 0; - - for (const index of decryptKey) { - const start = index[0] + currentIndex; - const end = start + index[1]; - - for (let i = start; i < end; i++) { - extractedKey += res.data.sources[i]; - sourcesArray[i] = ""; - } - currentIndex += index[1]; - } - - decryptKey = extractedKey; - sources = sourcesArray.join(""); - - const decrypt = CryptoJS.AES.decrypt(sources, decryptKey); - sources = JSON.parse(decrypt.toString(CryptoJS.enc.Utf8)); - } - } catch (err: any) { - console.log(err.message); - throw new Error("Cannot decrypt sources. Perhaps the key is invalid."); - } - - this.sources = sources?.map((s: any) => ({ - url: s.file, - isM3U8: s.file.includes(".m3u8"), - })); - - result.sources.push(...this.sources); - - if (videoUrl.href.includes(new URL(this.host).host)) { - result.sources = []; - this.sources = []; - - for (const source of sources) { - const { data } = await axios.get(source.file, options); - const m3u8data = data - .split("\n") - .filter( - (line: string) => - line.includes(".m3u8") && line.includes("RESOLUTION=") - ); - - const secondHalf = m3u8data.map((line: string) => - line.match(/RESOLUTION=.*,(C)|URI=.*/g)?.map((s) => s.split("=")[1]) - ); - - const TdArray = secondHalf.map((s: string[]) => { - const f1 = s[0].split(",C")[0]; - const f2 = s[1].replace(/"/g, ""); - - return [f1, f2]; - }); - - for (const [f1, f2] of TdArray) { - this.sources.push({ - url: `${source.file?.split("master.m3u8")[0]}${f2.replace( - "iframes", - "index" - )}`, - quality: f1.split("x")[1] + "p", - isM3U8: f2.includes(".m3u8"), - }); - } - result.sources.push(...this.sources); - } - } - - result.intro = - intro?.end > 1 ? { start: intro.start, end: intro.end } : undefined; - result.outro = - outro?.end > 1 ? { start: outro.start, end: outro.end } : undefined; - - result.sources.push({ - url: sources[0].file, - isM3U8: sources[0].file.includes(".m3u8"), - quality: "auto", - }); - - result.subtitles = tracks - .map((s: any) => - s.file - ? { url: s.file, lang: s.label ? s.label : "Thumbnails" } - : null - ) - .filter((s: any) => s); - - return result; - } catch (err: any) { - console.log(err.message); - throw err; - } - } -} - -export default RapidCloud; diff --git a/src/extractors/streamsb.ts b/src/extractors/streamsb.ts deleted file mode 100644 index 3eeaabe..0000000 --- a/src/extractors/streamsb.ts +++ /dev/null @@ -1,83 +0,0 @@ -import axios from "axios"; -import type { Video } from "../types/extractor.js"; -import { USER_AGENT_HEADER } from "../utils/index.js"; - -class StreamSB { - private serverName = "streamSB"; - private sources: Video[] = []; - - private readonly host = "https://watchsb.com/sources50"; - private readonly host2 = "https://streamsss.net/sources16"; - - private PAYLOAD(hex: string): string { - // `5363587530696d33443675687c7c${hex}7c7c433569475830474c497a65767c7c73747265616d7362`; - return `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`; - } - - async extract(videoUrl: URL, isAlt: boolean = false): Promise<Video[]> { - let headers: Record<string, string> = { - watchsb: "sbstream", - Referer: videoUrl.href, - "User-Agent": USER_AGENT_HEADER, - }; - let id = videoUrl.href.split("/e/").pop(); - if (id?.includes("html")) { - id = id.split(".html")[0]; - } - const bytes = new TextEncoder().encode(id); - - const res = await axios - .get( - `${isAlt ? this.host2 : this.host}/${this.PAYLOAD( - Buffer.from(bytes).toString("hex") - )}`, - { headers } - ) - .catch(() => null); - - if (!res?.data.stream_data) { - throw new Error("No source found. Try a different server"); - } - - headers = { - "User-Agent": USER_AGENT_HEADER, - Referer: videoUrl.href.split("e/")[0], - }; - - const m3u8_urls = await axios.get(res.data.stream_data.file, { - headers, - }); - - const videoList = m3u8_urls?.data?.split("#EXT-X-STREAM-INF:") ?? []; - - for (const video of videoList) { - if (!video.includes("m3u8")) continue; - - const url = video.split("\n")[1]; - const quality = video.split("RESOLUTION=")[1].split(",")[0].split("x")[1]; - - this.sources.push({ - url: url, - quality: `${quality}p`, - isM3U8: true, - }); - } - - this.sources.push({ - url: res.data.stream_data.file, - quality: "auto", - isM3U8: res.data.stream_data.file.includes(".m3u8"), - }); - - return this.sources; - } - - private addSources(source: any): void { - this.sources.push({ - url: source.file, - isM3U8: source.file.includes(".m3u8"), - }); - } -} - -export default StreamSB; diff --git a/src/extractors/streamtape.ts b/src/extractors/streamtape.ts deleted file mode 100644 index 69910ce..0000000 --- a/src/extractors/streamtape.ts +++ /dev/null @@ -1,37 +0,0 @@ -import axios from "axios"; -import { load, type CheerioAPI } from "cheerio"; -import type { Video } from "../types/extractor.js"; - -class StreamTape { - private serverName = "StreamTape"; - private sources: Video[] = []; - - async extract(videoUrl: URL): Promise<Video[]> { - try { - const { data } = await axios.get(videoUrl.href).catch(() => { - throw new Error("Video not found"); - }); - - const $: CheerioAPI = load(data); - - let [fh, sh] = $.html() - ?.match(/robotlink'\).innerHTML = (.*)'/)![1] - .split("+ ('"); - - sh = sh.substring(3); - fh = fh.replace(/\'/g, ""); - - const url = `https:${fh}${sh}`; - - this.sources.push({ - url: url, - isM3U8: url.includes(".m3u8"), - }); - - return this.sources; - } catch (err) { - throw new Error((err as Error).message); - } - } -} -export default StreamTape; diff --git a/src/parsers/animeAboutInfo.ts b/src/parsers/animeAboutInfo.ts deleted file mode 100644 index f92b0fc..0000000 --- a/src/parsers/animeAboutInfo.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { - SRC_BASE_URL, - extractAnimes, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractMostPopularAnimes, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import { type ScrapedAnimeAboutInfo } from "../types/parsers/index.js"; - -// /anime/info?id=${anime-id} -async function scrapeAnimeAboutInfo( - id: string -): Promise<ScrapedAnimeAboutInfo | HttpError> { - const res: ScrapedAnimeAboutInfo = { - anime: { - info: { - id: null, - anilistId: null, - malId: null, - name: null, - poster: null, - description: null, - stats: { - rating: null, - quality: null, - episodes: { - sub: null, - dub: null, - }, - type: null, - duration: null, - }, - promotionalVideos: [], - charactersVoiceActors: [], - }, - moreInfo: {}, - }, - seasons: [], - mostPopularAnimes: [], - relatedAnimes: [], - recommendedAnimes: [], - }; - - try { - const animeUrl: URL = new URL(id, SRC_BASE_URL); - const mainPage = await axios.get(animeUrl.href, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - try { - res.anime.info.anilistId = Number( - JSON.parse($("body")?.find("#syncData")?.text())?.anilist_id - ); - res.anime.info.malId = Number(JSON.parse($("body")?.find("#syncData")?.text())?.mal_id); - } catch (err) { - res.anime.info.anilistId = null; - res.anime.info.malId = null; - } - - const selector: SelectorType = "#ani_detail .container .anis-content"; - - res.anime.info.id = - $(selector) - ?.find(".anisc-detail .film-buttons a.btn-play") - ?.attr("href") - ?.split("/") - ?.pop() || null; - res.anime.info.name = - $(selector) - ?.find(".anisc-detail .film-name.dynamic-name") - ?.text() - ?.trim() || null; - res.anime.info.description = - $(selector) - ?.find(".anisc-detail .film-description .text") - .text() - ?.split("[") - ?.shift() - ?.trim() || null; - res.anime.info.poster = - $(selector)?.find(".film-poster .film-poster-img")?.attr("src")?.trim() || - null; - - // stats - res.anime.info.stats.rating = - $(`${selector} .film-stats .tick .tick-pg`)?.text()?.trim() || null; - res.anime.info.stats.quality = - $(`${selector} .film-stats .tick .tick-quality`)?.text()?.trim() || null; - res.anime.info.stats.episodes = { - sub: - Number($(`${selector} .film-stats .tick .tick-sub`)?.text()?.trim()) || - null, - dub: - Number($(`${selector} .film-stats .tick .tick-dub`)?.text()?.trim()) || - null, - }; - res.anime.info.stats.type = - $(`${selector} .film-stats .tick`) - ?.text() - ?.trim() - ?.replace(/[\s\n]+/g, " ") - ?.split(" ") - ?.at(-2) || null; - res.anime.info.stats.duration = - $(`${selector} .film-stats .tick`) - ?.text() - ?.trim() - ?.replace(/[\s\n]+/g, " ") - ?.split(" ") - ?.pop() || null; - - // get promotional videos - $(".block_area.block_area-promotions .block_area-promotions-list .screen-items .item").each( - (_, el) => { - res.anime.info.promotionalVideos.push({ - title: $(el).attr("data-title"), - source: $(el).attr("data-src"), - thumbnail: $(el).find("img").attr("src"), - }); - } - ); - - // get characters and voice actors - $(".block_area.block_area-actors .block-actors-content .bac-list-wrap .bac-item").each( - (_, el) => { - res.anime.info.charactersVoiceActors.push({ - character: { - id: $(el).find($(".per-info.ltr .pi-avatar")).attr("href")?.split("/")[2] || "", - poster: $(el).find($(".per-info.ltr .pi-avatar img")).attr("data-src") || "", - name: $(el).find($(".per-info.ltr .pi-detail a")).text(), - cast: $(el).find($(".per-info.ltr .pi-detail .pi-cast")).text(), - }, - voiceActor: { - id: $(el).find($(".per-info.rtl .pi-avatar")).attr("href")?.split("/")[2] || "", - poster: $(el).find($(".per-info.rtl .pi-avatar img")).attr("data-src") || "", - name: $(el).find($(".per-info.rtl .pi-detail a")).text(), - cast: $(el).find($(".per-info.rtl .pi-detail .pi-cast")).text(), - }, - }); - } - ); - - // more information - $(`${selector} .anisc-info-wrap .anisc-info .item:not(.w-hide)`).each( - (i, el) => { - let key = $(el) - .find(".item-head") - .text() - .toLowerCase() - .replace(":", "") - .trim(); - key = key.includes(" ") ? key.replace(" ", "") : key; - - const value = [ - ...$(el) - .find("*:not(.item-head)") - .map((i, el) => $(el).text().trim()), - ] - .map((i) => `${i}`) - .toString() - .trim(); - - if (key === "genres") { - res.anime.moreInfo[key] = value.split(",").map((i) => i.trim()); - return; - } - if (key === "producers") { - res.anime.moreInfo[key] = value.split(",").map((i) => i.trim()); - return; - } - res.anime.moreInfo[key] = value; - } - ); - - // more seasons - const seasonsSelector: SelectorType = "#main-content .os-list a.os-item"; - $(seasonsSelector).each((i, el) => { - res.seasons.push({ - id: $(el)?.attr("href")?.slice(1)?.trim() || null, - name: $(el)?.attr("title")?.trim() || null, - title: $(el)?.find(".title")?.text()?.trim(), - poster: - $(el) - ?.find(".season-poster") - ?.attr("style") - ?.split(" ") - ?.pop() - ?.split("(") - ?.pop() - ?.split(")")[0] || null, - isCurrent: $(el).hasClass("active"), - }); - }); - - const relatedAnimeSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(1) .anif-block-ul ul li"; - res.relatedAnimes = extractMostPopularAnimes($, relatedAnimeSelector); - - const mostPopularSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime:nth-of-type(2) .anif-block-ul ul li"; - res.mostPopularAnimes = extractMostPopularAnimes($, mostPopularSelector); - - const recommendedAnimeSelector: SelectorType = - "#main-content .block_area.block_area_category .tab-content .flw-item"; - res.recommendedAnimes = extractAnimes($, recommendedAnimeSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeAboutInfo; diff --git a/src/parsers/animeCategory.ts b/src/parsers/animeCategory.ts deleted file mode 100644 index c79fcc7..0000000 --- a/src/parsers/animeCategory.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - SRC_BASE_URL, - extractAnimes, - ACCEPT_HEADER, - USER_AGENT_HEADER, - extractTop10Animes, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import { type AnimeCategories } from "../types/anime.js"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import { type ScrapedAnimeCategory } from "../types/parsers/index.js"; - -// /anime/:category?page=${page} -async function scrapeAnimeCategory( - category: AnimeCategories, - page: number = 1 -): Promise<ScrapedAnimeCategory | HttpError> { - const res: ScrapedAnimeCategory = { - animes: [], - genres: [], - top10Animes: { - today: [], - week: [], - month: [], - }, - category, - currentPage: Number(page), - hasNextPage: false, - totalPages: 1, - }; - - try { - const scrapeUrl: URL = new URL(category, SRC_BASE_URL); - const mainPage = await axios.get(`${scrapeUrl}?page=${page}`, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const selector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - const categoryNameSelector: SelectorType = - "#main-content .block_area .block_area-header .cat-heading"; - res.category = $(categoryNameSelector)?.text()?.trim() ?? category; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, selector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const genreSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; - $(genreSelector).each((i, el) => { - res.genres.push(`${$(el).text().trim()}`); - }); - - const top10AnimeSelector: SelectorType = - '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; - - $(top10AnimeSelector).each((i, el) => { - const period = $(el).attr("id")?.split("-")?.pop()?.trim(); - - if (period === "day") { - res.top10Animes.today = extractTop10Animes($, period); - return; - } - if (period === "week") { - res.top10Animes.week = extractTop10Animes($, period); - return; - } - if (period === "month") { - res.top10Animes.month = extractTop10Animes($, period); - } - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeCategory; diff --git a/src/parsers/animeEpisodeSrcs.ts b/src/parsers/animeEpisodeSrcs.ts deleted file mode 100644 index 913c535..0000000 --- a/src/parsers/animeEpisodeSrcs.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - SRC_AJAX_URL, - SRC_BASE_URL, - retrieveServerId, - USER_AGENT_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import { load, type CheerioAPI } from "cheerio"; -import createHttpError, { type HttpError } from "http-errors"; -import { type AnimeServers, Servers } from "../types/anime.js"; -import { - RapidCloud, - StreamSB, - StreamTape, - MegaCloud, -} from "../extractors/index.js"; -import { type ScrapedAnimeEpisodesSources } from "../types/parsers/index.js"; - -// vidtreaming -> 4 -// rapidcloud -> 1 -// streamsb -> 5 -// streamtape -> 3 - -// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} -async function scrapeAnimeEpisodeSources( - episodeId: string, - server: AnimeServers = Servers.VidStreaming, - category: "sub" | "dub" | "raw" = "sub" -): Promise<ScrapedAnimeEpisodesSources | HttpError> { - if (episodeId.startsWith("http")) { - const serverUrl = new URL(episodeId); - switch (server) { - case Servers.VidStreaming: - case Servers.VidCloud: - return { - ...(await new MegaCloud().extract(serverUrl)), - }; - case Servers.StreamSB: - return { - headers: { - Referer: serverUrl.href, - watchsb: "streamsb", - "User-Agent": USER_AGENT_HEADER, - }, - sources: await new StreamSB().extract(serverUrl, true), - }; - case Servers.StreamTape: - return { - headers: { Referer: serverUrl.href, "User-Agent": USER_AGENT_HEADER }, - sources: await new StreamTape().extract(serverUrl), - }; - default: // vidcloud - return { - headers: { Referer: serverUrl.href }, - ...(await new RapidCloud().extract(serverUrl)), - }; - } - } - - const epId = new URL(`/watch/${episodeId}`, SRC_BASE_URL).href; - console.log(epId); - - try { - const resp = await axios.get( - `${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId.split("?ep=")[1]}`, - { - headers: { - Referer: epId, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - }, - } - ); - - const $: CheerioAPI = load(resp.data.html); - - let serverId: string | null = null; - - try { - console.log("THE SERVER: ", server); - - switch (server) { - case Servers.VidCloud: { - serverId = retrieveServerId($, 1, category); - if (!serverId) throw new Error("RapidCloud not found"); - break; - } - case Servers.VidStreaming: { - serverId = retrieveServerId($, 4, category); - console.log("SERVER_ID: ", serverId); - if (!serverId) throw new Error("VidStreaming not found"); - break; - } - case Servers.StreamSB: { - serverId = retrieveServerId($, 5, category); - if (!serverId) throw new Error("StreamSB not found"); - break; - } - case Servers.StreamTape: { - serverId = retrieveServerId($, 3, category); - if (!serverId) throw new Error("StreamTape not found"); - break; - } - } - } catch (err) { - throw createHttpError.NotFound( - "Couldn't find server. Try another server" - ); - } - - const { - data: { link }, - } = await axios.get(`${SRC_AJAX_URL}/v2/episode/sources?id=${serverId}`); - console.log("THE LINK: ", link); - - return await scrapeAnimeEpisodeSources(link, server); - } catch (err: any) { - console.log(err); - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeEpisodeSources; diff --git a/src/parsers/animeEpisodes.ts b/src/parsers/animeEpisodes.ts deleted file mode 100644 index 41ac942..0000000 --- a/src/parsers/animeEpisodes.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - SRC_BASE_URL, - SRC_AJAX_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import { load, type CheerioAPI } from "cheerio"; -import createHttpError, { type HttpError } from "http-errors"; -import { type ScrapedAnimeEpisodes } from "../types/parsers/index.js"; - -// /anime/episodes/${anime-id} -async function scrapeAnimeEpisodes( - animeId: string -): Promise<ScrapedAnimeEpisodes | HttpError> { - const res: ScrapedAnimeEpisodes = { - totalEpisodes: 0, - episodes: [], - }; - - try { - const episodesAjax = await axios.get( - `${SRC_AJAX_URL}/v2/episode/list/${animeId.split("-").pop()}`, - { - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Referer: `${SRC_BASE_URL}/watch/${animeId}`, - }, - } - ); - - const $: CheerioAPI = load(episodesAjax.data.html); - - res.totalEpisodes = Number($(".detail-infor-content .ss-list a").length); - - $(".detail-infor-content .ss-list a").each((i, el) => { - res.episodes.push({ - title: $(el)?.attr("title")?.trim() || null, - episodeId: $(el)?.attr("href")?.split("/")?.pop() || null, - number: Number($(el).attr("data-number")), - isFiller: $(el).hasClass("ssl-item-filler"), - }); - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeEpisodes; diff --git a/src/parsers/animeGenre.ts b/src/parsers/animeGenre.ts deleted file mode 100644 index 110398d..0000000 --- a/src/parsers/animeGenre.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - SRC_BASE_URL, - ACCEPT_HEADER, - extractAnimes, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractMostPopularAnimes, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import type { ScrapedGenreAnime } from "../types/parsers/index.js"; - -// /anime/genre/${name}?page=${page} -async function scrapeGenreAnime( - genreName: string, - page: number = 1 -): Promise<ScrapedGenreAnime | HttpError> { - const res: ScrapedGenreAnime = { - genreName, - animes: [], - genres: [], - topAiringAnimes: [], - totalPages: 1, - hasNextPage: false, - currentPage: Number(page), - }; - - // there's a typo with zoro where martial arts is marial arts - genreName = genreName === "martial-arts" ? "marial-arts" : genreName; - - try { - const genreUrl: URL = new URL( - `/genre/${genreName}?page=${page}`, - SRC_BASE_URL - ); - - const mainPage = await axios.get(genreUrl.href, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const selector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - const genreNameSelector: SelectorType = - "#main-content .block_area .block_area-header .cat-heading"; - res.genreName = $(genreNameSelector)?.text()?.trim() ?? genreName; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, selector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const genreSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; - $(genreSelector).each((i, el) => { - res.genres.push(`${$(el).text().trim()}`); - }); - - const topAiringSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; - res.topAiringAnimes = extractMostPopularAnimes($, topAiringSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeGenreAnime; diff --git a/src/parsers/animeProducer.ts b/src/parsers/animeProducer.ts deleted file mode 100644 index 2862852..0000000 --- a/src/parsers/animeProducer.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - SRC_BASE_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractMostPopularAnimes, - extractAnimes, - extractTop10Animes, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import type { ScrapedProducerAnime } from "../types/parsers/index.js"; - -// /anime/producer/${name}?page=${page} -async function scrapeProducerAnimes( - producerName: string, - page: number = 1 -): Promise<ScrapedProducerAnime | HttpError> { - const res: ScrapedProducerAnime = { - producerName, - animes: [], - top10Animes: { - today: [], - week: [], - month: [], - }, - topAiringAnimes: [], - totalPages: 1, - hasNextPage: false, - currentPage: Number(page), - }; - - try { - const producerUrl: URL = new URL( - `/producer/${producerName}?page=${page}`, - SRC_BASE_URL - ); - - const mainPage = await axios.get(producerUrl.href, { - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const animeSelector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, animeSelector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const producerNameSelector: SelectorType = - "#main-content .block_area .block_area-header .cat-heading"; - res.producerName = $(producerNameSelector)?.text()?.trim() ?? producerName; - - const top10AnimeSelector: SelectorType = - '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; - - $(top10AnimeSelector).each((_, el) => { - const period = $(el).attr("id")?.split("-")?.pop()?.trim(); - - if (period === "day") { - res.top10Animes.today = extractTop10Animes($, period); - return; - } - if (period === "week") { - res.top10Animes.week = extractTop10Animes($, period); - return; - } - if (period === "month") { - res.top10Animes.month = extractTop10Animes($, period); - } - }); - - const topAiringSelector: SelectorType = - "#main-sidebar .block_area_sidebar:nth-child(2) .block_area-content .anif-block-ul ul li"; - res.topAiringAnimes = extractMostPopularAnimes($, topAiringSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeProducerAnimes; diff --git a/src/parsers/animeSearch.ts b/src/parsers/animeSearch.ts deleted file mode 100644 index 16818c6..0000000 --- a/src/parsers/animeSearch.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - SRC_SEARCH_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractAnimes, - getSearchFilterValue, - extractMostPopularAnimes, - getSearchDateFilterValue, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import type { ScrapedAnimeSearchResult } from "../types/parsers/index.js"; -import type { SearchFilters, FilterKeys } from "../types/controllers/index.js"; - -// /anime/search?q=${query}&page=${page} -async function scrapeAnimeSearch( - q: string, - page: number = 1, - filters: SearchFilters -): Promise<ScrapedAnimeSearchResult | HttpError> { - const res: ScrapedAnimeSearchResult = { - animes: [], - mostPopularAnimes: [], - currentPage: Number(page), - hasNextPage: false, - totalPages: 1, - searchQuery: q, - searchFilters: filters, - }; - - try { - const url = new URL(SRC_SEARCH_URL); - url.searchParams.set("keyword", q); - url.searchParams.set("page", `${page}`); - url.searchParams.set("sort", "default"); - - for (const key in filters) { - if (key.includes("_date")) { - const dates = getSearchDateFilterValue( - key === "start_date", - filters[key as keyof SearchFilters] || "" - ); - if (!dates) continue; - - dates.map((dateParam) => { - const [key, val] = dateParam.split("="); - url.searchParams.set(key, val); - }); - continue; - } - - const filterVal = getSearchFilterValue( - key as FilterKeys, - filters[key as keyof SearchFilters] || "" - ); - filterVal && url.searchParams.set(key, filterVal); - } - - const mainPage = await axios.get(url.href, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const selector: SelectorType = - "#main-content .tab-content .film_list-wrap .flw-item"; - - res.hasNextPage = - $(".pagination > li").length > 0 - ? $(".pagination li.active").length > 0 - ? $(".pagination > li").last().hasClass("active") - ? false - : true - : false - : false; - - res.totalPages = - Number( - $('.pagination > .page-item a[title="Last"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $('.pagination > .page-item a[title="Next"]') - ?.attr("href") - ?.split("=") - .pop() ?? - $(".pagination > .page-item.active a")?.text()?.trim() - ) || 1; - - res.animes = extractAnimes($, selector); - - if (res.animes.length === 0 && !res.hasNextPage) { - res.totalPages = 0; - } - - const mostPopularSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-realtime .anif-block-ul ul li"; - res.mostPopularAnimes = extractMostPopularAnimes($, mostPopularSelector); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeSearch; diff --git a/src/parsers/animeSearchSuggestion.ts b/src/parsers/animeSearchSuggestion.ts deleted file mode 100644 index acef2a4..0000000 --- a/src/parsers/animeSearchSuggestion.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - SRC_HOME_URL, - SRC_AJAX_URL, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import type { ScrapedAnimeSearchSuggestion } from "../types/parsers/index.js"; - -// /anime/search/suggest?q=${query} -async function scrapeAnimeSearchSuggestion( - q: string -): Promise<ScrapedAnimeSearchSuggestion | HttpError> { - const res: ScrapedAnimeSearchSuggestion = { - suggestions: [], - }; - - try { - const { data } = await axios.get( - `${SRC_AJAX_URL}/search/suggest?keyword=${encodeURIComponent(q)}`, - { - headers: { - Accept: "*/*", - Pragma: "no-cache", - Referer: SRC_HOME_URL, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, - } - ); - - const $: CheerioAPI = load(data.html); - const selector: SelectorType = ".nav-item:has(.film-poster)"; - - if ($(selector).length < 1) return res; - - $(selector).each((_, el) => { - const id = $(el).attr("href")?.split("?")[0].includes("javascript") - ? null - : $(el).attr("href")?.split("?")[0]?.slice(1); - - res.suggestions.push({ - id, - name: $(el).find(".srp-detail .film-name")?.text()?.trim() || null, - jname: - $(el).find(".srp-detail .film-name")?.attr("data-jname")?.trim() || - $(el).find(".srp-detail .alias-name")?.text()?.trim() || - null, - poster: $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim(), - moreInfo: [ - ...$(el) - .find(".film-infor") - .contents() - .map((_, el) => $(el).text().trim()), - ].filter((i) => i), - }); - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeAnimeSearchSuggestion; diff --git a/src/parsers/episodeServers.ts b/src/parsers/episodeServers.ts deleted file mode 100644 index 8f95229..0000000 --- a/src/parsers/episodeServers.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - SRC_BASE_URL, - SRC_AJAX_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import type { ScrapedEpisodeServers } from "../types/parsers/index.js"; - -// /anime/servers?episodeId=${id} -async function scrapeEpisodeServers( - episodeId: string -): Promise<ScrapedEpisodeServers | HttpError> { - const res: ScrapedEpisodeServers = { - sub: [], - dub: [], - raw: [], - episodeId, - episodeNo: 0, - }; - - try { - const epId = episodeId.split("?ep=")[1]; - - const { data } = await axios.get( - `${SRC_AJAX_URL}/v2/episode/servers?episodeId=${epId}`, - { - headers: { - Accept: ACCEPT_HEADER, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Referer: new URL(`/watch/${episodeId}`, SRC_BASE_URL).href, - }, - } - ); - - const $: CheerioAPI = load(data.html); - - const epNoSelector: SelectorType = ".server-notice strong"; - res.episodeNo = Number($(epNoSelector).text().split(" ").pop()) || 0; - - $(`.ps_-block.ps_-block-sub.servers-sub .ps__-list .server-item`).each( - (_, el) => { - res.sub.push({ - serverName: $(el).find("a").text().toLowerCase().trim(), - serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, - }); - } - ); - - $(`.ps_-block.ps_-block-sub.servers-dub .ps__-list .server-item`).each( - (_, el) => { - res.dub.push({ - serverName: $(el).find("a").text().toLowerCase().trim(), - serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, - }); - } - ); - - $(`.ps_-block.ps_-block-sub.servers-raw .ps__-list .server-item`).each( - (_, el) => { - res.raw.push({ - serverName: $(el).find("a").text().toLowerCase().trim(), - serverId: Number($(el)?.attr("data-server-id")?.trim()) || null, - }); - } - ); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeEpisodeServers; diff --git a/src/parsers/estimatedSchedule.ts b/src/parsers/estimatedSchedule.ts deleted file mode 100644 index abfce97..0000000 --- a/src/parsers/estimatedSchedule.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - SRC_HOME_URL, - SRC_AJAX_URL, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; -import { type ScrapedEstimatedSchedule } from "../types/parsers/index.js"; - -// /anime/schedule?date=${date} -async function scrapeEstimatedSchedule( - date: string -): Promise<ScrapedEstimatedSchedule | HttpError> { - const res: ScrapedEstimatedSchedule = { - scheduledAnimes: [], - }; - - try { - const estScheduleURL = - `${SRC_AJAX_URL}/schedule/list?tzOffset=-330&date=${date}` as const; - - const mainPage = await axios.get(estScheduleURL, { - headers: { - Accept: "*/*", - Referer: SRC_HOME_URL, - "User-Agent": USER_AGENT_HEADER, - "X-Requested-With": "XMLHttpRequest", - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage?.data?.html); - - const selector: SelectorType = "li"; - - if ($(selector)?.text()?.trim()?.includes("No data to display")) { - return res; - } - - $(selector).each((_, el) => { - const airingTimestamp = new Date( - `${date}T${$(el)?.find("a .time")?.text()?.trim()}:00` - ).getTime(); - - res.scheduledAnimes.push({ - id: $(el)?.find("a")?.attr("href")?.slice(1)?.trim() || null, - time: $(el)?.find("a .time")?.text()?.trim() || null, - name: $(el)?.find("a .film-name.dynamic-name")?.text()?.trim() || null, - jname: - $(el) - ?.find("a .film-name.dynamic-name") - ?.attr("data-jname") - ?.trim() || null, - airingTimestamp, - secondsUntilAiring: Math.floor((airingTimestamp - Date.now()) / 1000), - episode: Number($(el).find("a .fd-play button").text().trim().split(" ")[1]) - }); - }); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeEstimatedSchedule; diff --git a/src/parsers/homePage.ts b/src/parsers/homePage.ts deleted file mode 100644 index cbdaef6..0000000 --- a/src/parsers/homePage.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { - SRC_HOME_URL, - ACCEPT_HEADER, - USER_AGENT_HEADER, - ACCEPT_ENCODING_HEADER, - extractTop10Animes, - extractAnimes, - extractMostPopularAnimes, -} from "../utils/index.js"; -import axios, { AxiosError } from "axios"; -import createHttpError, { type HttpError } from "http-errors"; -import type { ScrapedHomePage } from "../types/parsers/index.js"; -import { load, type CheerioAPI, type SelectorType } from "cheerio"; - -// /anime/home -async function scrapeHomePage(): Promise<ScrapedHomePage | HttpError> { - const res: ScrapedHomePage = { - spotlightAnimes: [], - trendingAnimes: [], - latestEpisodeAnimes: [], - topUpcomingAnimes: [], - top10Animes: { - today: [], - week: [], - month: [], - }, - topAiringAnimes: [], - mostPopularAnimes: [], - mostFavoriteAnimes: [], - latestCompletedAnimes: [], - genres: [], - }; - - try { - const mainPage = await axios.get(SRC_HOME_URL as string, { - headers: { - "User-Agent": USER_AGENT_HEADER, - "Accept-Encoding": ACCEPT_ENCODING_HEADER, - Accept: ACCEPT_HEADER, - }, - }); - - const $: CheerioAPI = load(mainPage.data); - - const spotlightSelector: SelectorType = - "#slider .swiper-wrapper .swiper-slide"; - - $(spotlightSelector).each((i, el) => { - const otherInfo = $(el) - .find(".deslide-item-content .sc-detail .scd-item") - .map((i, el) => $(el).text().trim()) - .get() - .slice(0, -1); - - res.spotlightAnimes.push({ - rank: - Number( - $(el) - .find(".deslide-item-content .desi-sub-text") - ?.text() - .trim() - .split(" ")[0] - .slice(1) - ) || null, - id: $(el) - .find(".deslide-item-content .desi-buttons a") - ?.last() - ?.attr("href") - ?.slice(1) - ?.trim(), - name: $(el) - .find(".deslide-item-content .desi-head-title.dynamic-name") - ?.text() - .trim(), - description: $(el) - .find(".deslide-item-content .desi-description") - ?.text() - ?.split("[") - ?.shift() - ?.trim(), - poster: $(el) - .find(".deslide-cover .deslide-cover-img .film-poster-img") - ?.attr("data-src") - ?.trim(), - jname: $(el) - .find(".deslide-item-content .desi-head-title.dynamic-name") - ?.attr("data-jname") - ?.trim(), - episodes: { - sub: - Number( - $(el) - .find( - ".deslide-item-content .sc-detail .scd-item .tick-item.tick-sub" - ) - ?.text() - ?.trim() - ) || null, - dub: - Number( - $(el) - .find( - ".deslide-item-content .sc-detail .scd-item .tick-item.tick-dub" - ) - ?.text() - ?.trim() - ) || null, - }, - otherInfo, - }); - }); - - const trendingSelector: SelectorType = - "#trending-home .swiper-wrapper .swiper-slide"; - - $(trendingSelector).each((i, el) => { - res.trendingAnimes.push({ - rank: parseInt( - $(el).find(".item .number")?.children()?.first()?.text()?.trim() - ), - id: $(el).find(".item .film-poster")?.attr("href")?.slice(1)?.trim(), - name: $(el) - .find(".item .number .film-title.dynamic-name") - ?.text() - ?.trim(), - jname: $(el) - .find(".item .number .film-title.dynamic-name") - ?.attr("data-jname") - ?.trim(), - poster: $(el) - .find(".item .film-poster .film-poster-img") - ?.attr("data-src") - ?.trim(), - }); - }); - - const latestEpisodeSelector: SelectorType = - "#main-content .block_area_home:nth-of-type(1) .tab-content .film_list-wrap .flw-item"; - res.latestEpisodeAnimes = extractAnimes($, latestEpisodeSelector); - - const topUpcomingSelector: SelectorType = - "#main-content .block_area_home:nth-of-type(3) .tab-content .film_list-wrap .flw-item"; - res.topUpcomingAnimes = extractAnimes($, topUpcomingSelector); - - const genreSelector: SelectorType = - "#main-sidebar .block_area.block_area_sidebar.block_area-genres .sb-genre-list li"; - $(genreSelector).each((i, el) => { - res.genres.push(`${$(el).text().trim()}`); - }); - - const mostViewedSelector: SelectorType = - '#main-sidebar .block_area-realtime [id^="top-viewed-"]'; - $(mostViewedSelector).each((i, el) => { - const period = $(el).attr("id")?.split("-")?.pop()?.trim(); - - if (period === "day") { - res.top10Animes.today = extractTop10Animes($, period); - return; - } - if (period === "week") { - res.top10Animes.week = extractTop10Animes($, period); - return; - } - if (period === "month") { - res.top10Animes.month = extractTop10Animes($, period); - } - }); - - res.topAiringAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(1) .anif-block-ul ul li"); - res.mostPopularAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(2) .anif-block-ul ul li"); - res.mostFavoriteAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(3) .anif-block-ul ul li"); - res.latestCompletedAnimes = extractMostPopularAnimes($, "#anime-featured .row div:nth-of-type(4) .anif-block-ul ul li"); - - return res; - } catch (err: any) { - if (err instanceof AxiosError) { - throw createHttpError( - err?.response?.status || 500, - err?.response?.statusText || "Something went wrong" - ); - } - throw createHttpError.InternalServerError(err?.message); - } -} - -export default scrapeHomePage; diff --git a/src/parsers/index.ts b/src/parsers/index.ts deleted file mode 100644 index 395833a..0000000 --- a/src/parsers/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import scrapeHomePage from "./homePage.js"; -import scrapeGenreAnime from "./animeGenre.js"; -import scrapeAnimeSearch from "./animeSearch.js"; -import scrapeAnimeEpisodes from "./animeEpisodes.js"; -import scrapeAnimeCategory from "./animeCategory.js"; -import scrapeProducerAnimes from "./animeProducer.js"; -import scrapeEpisodeServers from "./episodeServers.js"; -import scrapeAnimeAboutInfo from "./animeAboutInfo.js"; -import scrapeEstimatedSchedule from "./estimatedSchedule.js"; -import scrapeAnimeEpisodeSources from "./animeEpisodeSrcs.js"; -import scrapeAnimeSearchSuggestion from "./animeSearchSuggestion.js"; - -export { - scrapeHomePage, - scrapeGenreAnime, - scrapeAnimeSearch, - scrapeAnimeEpisodes, - scrapeAnimeCategory, - scrapeEpisodeServers, - scrapeProducerAnimes, - scrapeAnimeAboutInfo, - scrapeEstimatedSchedule, - scrapeAnimeEpisodeSources, - scrapeAnimeSearchSuggestion, -}; diff --git a/src/routes/hianime.ts b/src/routes/hianime.ts new file mode 100644 index 0000000..f4e1ff9 --- /dev/null +++ b/src/routes/hianime.ts @@ -0,0 +1,125 @@ +import { Hono } from "hono"; +import { HiAnime } from "aniwatch"; + +const hianime = new HiAnime.Scraper(); +const hianimeRouter = new Hono(); + +// /api/v2/hianime +hianimeRouter.get("/", (c) => c.redirect("/", 301)); + +// /api/v2/hianime/home +hianimeRouter.get("/home", async (c) => { + const data = await hianime.getHomePage(); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/category/{name}?page={page} +hianimeRouter.get("/category/:name", async (c) => { + const categoryName = decodeURIComponent( + c.req.param("name").trim() + ) as HiAnime.AnimeCategories; + + const page: number = + Number(decodeURIComponent(c.req.query("page") || "")) || 1; + + const data = await hianime.getCategoryAnime(categoryName, page); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/genre/{name}?page={page} +hianimeRouter.get("/genre/:name", async (c) => { + const genreName = decodeURIComponent(c.req.param("name").trim()); + const page: number = + Number(decodeURIComponent(c.req.query("page") || "")) || 1; + + const data = await hianime.getGenreAnime(genreName, page); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/producer/{name}?page={page} +hianimeRouter.get("/producer/:name", async (c) => { + const producerName = decodeURIComponent(c.req.param("name").trim()); + const page: number = + Number(decodeURIComponent(c.req.query("page") || "")) || 1; + + const data = await hianime.getProducerAnimes(producerName, page); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/schedule?date={date} +hianimeRouter.get("/schedule", async (c) => { + const date = decodeURIComponent(c.req.query("date") || ""); + + const data = await hianime.getEstimatedSchedule(date); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/search?q={query}&page={page}&filters={...filters} +hianimeRouter.get("/search", async (c) => { + let { q: query, page, ...filters } = c.req.query(); + + query = decodeURIComponent(query || ""); + const pageNo = Number(decodeURIComponent(page || "")) || 1; + + const data = await hianime.search(query, pageNo, filters); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/search/suggestion?q={query} +hianimeRouter.get("/search/suggestion", async (c) => { + const query = decodeURIComponent(c.req.query("q") || ""); + + const data = await hianime.searchSuggestions(query); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/anime/{animeId} +hianimeRouter.get("/anime/:animeId", async (c) => { + const animeId = decodeURIComponent(c.req.param("animeId").trim()); + const data = await hianime.getInfo(animeId); + + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/episode/servers?animeEpisodeId={id} +hianimeRouter.get("/episode/servers", async (c) => { + const animeEpisodeId = decodeURIComponent( + c.req.query("animeEpisodeId") || "" + ); + + const data = await hianime.getEpisodeServers(animeEpisodeId); + return c.json({ success: true, data }, { status: 200 }); +}); + +// episodeId=steinsgate-3?ep=230 +// /api/v2/hianime/episode/sources?animeEpisodeId={episodeId}?server={server}&category={category (dub or sub)} +hianimeRouter.get("/episode/sources", async (c) => { + const animeEpisodeId = decodeURIComponent( + c.req.query("animeEpisodeId") || "" + ); + const server = decodeURIComponent( + c.req.query("server") || HiAnime.Servers.VidStreaming + ) as HiAnime.AnimeServers; + + const category = decodeURIComponent(c.req.query("category") || "sub") as + | "sub" + | "dub" + | "raw"; + + const data = await hianime.getEpisodeSources( + animeEpisodeId, + server, + category + ); + return c.json({ success: true, data }, { status: 200 }); +}); + +// /api/v2/hianime/anime/{anime-id}/episodes +hianimeRouter.get("/anime/:animeId/episodes", async (c) => { + const animeId = decodeURIComponent(c.req.param("animeId").trim()); + const data = await hianime.getEpisodes(animeId); + + return c.json({ success: true, data }, { status: 200 }); +}); + +export { hianimeRouter }; diff --git a/src/routes/index.ts b/src/routes/index.ts deleted file mode 100644 index 7046ae1..0000000 --- a/src/routes/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Router, type IRouter } from "express"; -import { - getGenreAnime, - getAnimeSearch, - getHomePageInfo, - getAnimeCategory, - getAnimeEpisodes, - getEpisodeServers, - getProducerAnimes, - getAnimeAboutInfo, - getEstimatedSchedule, - getAnimeEpisodeSources, - getAnimeSearchSuggestion, -} from "../controllers/index.js"; - -const router: IRouter = Router(); - -// /anime -router.get("/", (_, res) => res.redirect("/")); - -// /anime/home -router.get("/home", getHomePageInfo); - -// /anime/info?id=${anime-id} -router.get("/info", getAnimeAboutInfo); - -// /anime/genre/${name}?page=${page} -router.get("/genre/:name", getGenreAnime); - -// /anime/search?q=${query}&page=${page} -router.get("/search", getAnimeSearch); - -// /anime/search/suggest?q=${query} -router.get("/search/suggest", getAnimeSearchSuggestion); - -// /anime/episodes/${anime-id} -router.get("/episodes/:animeId", getAnimeEpisodes); - -// /anime/servers?episodeId=${id} -router.get("/servers", getEpisodeServers); - -// episodeId=steinsgate-3?ep=230 -// /anime/episode-srcs?id=${episodeId}?server=${server}&category=${category (dub or sub)} -router.get("/episode-srcs", getAnimeEpisodeSources); - -// /anime/schedule?date=${date} -router.get("/schedule", getEstimatedSchedule); - -// /anime/producer/${name}?page=${page} -router.get("/producer/:name", getProducerAnimes); - -// /anime/:category?page=${page} -router.get("/:category", getAnimeCategory); - -export default router; diff --git a/src/server.ts b/src/server.ts index 3520958..6a9c06e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,59 +1,85 @@ import https from "https"; -import morgan from "morgan"; -import express from "express"; -import { resolve } from "path"; import { config } from "dotenv"; import corsConfig from "./config/cors.js"; import { ratelimit } from "./config/ratelimit.js"; -import errorHandler from "./config/errorHandler.js"; -import notFoundHandler from "./config/notFoundHandler.js"; -import animeRouter from "./routes/index.js"; +import { hianimeRouter } from "./routes/hianime.js"; + +import { serveStatic } from "@hono/node-server/serve-static"; +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { logger } from "hono/logger"; + +import { HiAnimeError } from "aniwatch"; config(); -const app: express.Application = express(); -const PORT: number = Number(process.env.PORT) || 4000; -app.use(morgan("dev")); +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(); + +app.use(logger()); app.use(corsConfig); // CAUTION: For personal deployments, "refrain" from having an env // named "ANIWATCH_API_HOSTNAME". You may face rate limitting -// and other issues if you do. -const ISNT_PERSONAL_DEPLOYMENT = Boolean(process?.env?.ANIWATCH_API_HOSTNAME); +// or other issues if you do. +const ISNT_PERSONAL_DEPLOYMENT = Boolean(ANIWATCH_API_HOSTNAME); if (ISNT_PERSONAL_DEPLOYMENT) { app.use(ratelimit); } -app.use(express.static(resolve("public"))); -app.get("/health", (_, res) => res.sendStatus(200)); -app.use("/anime", animeRouter); +app.use("/", serveStatic({ root: "public" })); +app.get("/health", (c) => c.text("OK", { status: 200 })); + +app.basePath(BASE_PATH).route("/hianime", hianimeRouter); +app + .basePath(BASE_PATH) + .get("/anicrush", (c) => c.text("Anicrush could be implemented in future.")); -app.use(notFoundHandler); -app.use(errorHandler); +app.notFound((c) => + c.json({ status: 404, message: "Resource Not Found" }, 404) +); + +app.onError((err, c) => { + console.error(err); + const res = { status: 500, message: "Internal Server Error" }; + + if (err instanceof HiAnimeError) { + res.status = err.status; + res.message = err.message; + } + + return c.json(res, { status: res.status }); +}); // NOTE: this env is "required" for vercel deployments -if (!Boolean(process?.env?.IS_VERCEL_DEPLOYMENT)) { - app.listen(PORT, () => { - console.log(`⚔️ api @ http://localhost:${PORT}`); - }); +if (!Boolean(process?.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) { + serve({ + port: PORT, + fetch: app.fetch, + }).addListener("listening", () => + console.info( + "\x1b[1;36m" + `aniwatch-api at http://localhost:${PORT}` + "\x1b[0m" + ) + ); // NOTE: remove the `if` block below for personal deployments if (ISNT_PERSONAL_DEPLOYMENT) { + const interval = 9 * 60 * 1000; // 9mins + // don't sleep - const intervalTime = 9 * 60 * 1000; // 9mins setInterval(() => { - console.log("HEALTHCHECK ;)", new Date().toLocaleString()); + console.log("aniwatch-api HEALTH_CHECK at", new Date().toISOString()); https - .get( - new URL("/health", `https://${process.env.ANIWATCH_API_HOSTNAME}`) - .href - ) + .get(`https://${ANIWATCH_API_HOSTNAME}/health`) .on("error", (err) => { console.error(err.message); }); - }, intervalTime); + }, interval); } } diff --git a/src/types/anime.ts b/src/types/anime.ts deleted file mode 100644 index 83a3d1f..0000000 --- a/src/types/anime.ts +++ /dev/null @@ -1,140 +0,0 @@ -export interface Anime { - id: string | null; - name: string | null; - jname: string | null; - poster: string | null; - duration: string | null; - type: string | null; - rating: string | null; - episodes: { - sub: number | null; - dub: number | null; - }; -} - -type CommonAnimeProps = "id" | "name" | "poster"; - -export interface Top10Anime extends Pick<Anime, CommonAnimeProps | "episodes"> { - rank: number | null; - jname: string | null; -} - -export type Top10AnimeTimePeriod = "day" | "week" | "month"; - -export interface MostPopularAnime - extends Pick<Anime, CommonAnimeProps | "episodes" | "type"> { - jname: string | null; -} - -export interface SpotlightAnime - extends MostPopularAnime, - Pick<Top10Anime, "rank"> { - description: string | null; -} - -export interface TrendingAnime - extends Pick<Anime, CommonAnimeProps>, - Pick<Top10Anime, "rank"> {} - -export interface LatestEpisodeAnime extends Anime {} - -export interface TopUpcomingAnime extends Anime {} - -export interface TopAiringAnime extends MostPopularAnime {} -export interface MostFavoriteAnime extends MostPopularAnime {} -export interface LatestCompletedAnime extends MostPopularAnime {} - -export interface AnimeGeneralAboutInfo - extends Pick<Anime, CommonAnimeProps>, - Pick<SpotlightAnime, "description"> { - anilistId: number | null; - malId: number | null; - stats: { - quality: string | null; - } & Pick<Anime, "duration" | "episodes" | "rating" | "type">; - promotionalVideos: AnimePromotionalVideo[]; - charactersVoiceActors: AnimeCharactersAndVoiceActors[]; -} - -export interface RecommendedAnime extends Anime {} - -export interface RelatedAnime extends MostPopularAnime {} - -export interface Season extends Pick<Anime, CommonAnimeProps> { - isCurrent: boolean; - title: string | null; -} - -export interface AnimePromotionalVideo { - title: string | undefined; - source: string | undefined; - thumbnail: string | undefined; -} - -export interface AnimeCharactersAndVoiceActors { - character: AnimeCharacter; - voiceActor: AnimeCharacter; -} - -export interface AnimeCharacter { - id: string; - poster: string; - name: string; - cast: string; -} - -export interface AnimeSearchSuggestion - extends Omit<MostPopularAnime, "episodes" | "type"> { - moreInfo: Array<string>; -} - -export interface AnimeEpisode extends Pick<Season, "title"> { - episodeId: string | null; - number: number; - isFiller: boolean; -} - -export interface SubEpisode { - serverName: string; - serverId: number | null; -} -export interface DubEpisode extends SubEpisode {} -export interface RawEpisode extends SubEpisode {} - -export type AnimeCategories = - | "most-favorite" - | "most-popular" - | "subbed-anime" - | "dubbed-anime" - | "recently-updated" - | "recently-added" - | "top-upcoming" - | "top-airing" - | "movie" - | "special" - | "ova" - | "ona" - | "tv" - | "completed"; - -export type AnimeServers = - | "hd-1" - | "hd-2" - | "megacloud" - | "streamsb" - | "streamtape"; - -export enum Servers { - VidStreaming = "hd-1", - MegaCloud = "megacloud", - StreamSB = "streamsb", - StreamTape = "streamtape", - VidCloud = "hd-2", - AsianLoad = "asianload", - GogoCDN = "gogocdn", - MixDrop = "mixdrop", - UpCloud = "upcloud", - VizCloud = "vizcloud", - MyCloud = "mycloud", - Filemoon = "filemoon", -} diff --git a/src/types/controllers/animeAboutInfo.ts b/src/types/controllers/animeAboutInfo.ts deleted file mode 100644 index aa65efd..0000000 --- a/src/types/controllers/animeAboutInfo.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AnimeAboutInfoQueryParams = { - id?: string; -}; diff --git a/src/types/controllers/animeCategory.ts b/src/types/controllers/animeCategory.ts deleted file mode 100644 index 7de0876..0000000 --- a/src/types/controllers/animeCategory.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type CategoryAnimePathParams = { - category?: string; -}; - -export type CategoryAnimeQueryParams = { - page?: string; -}; diff --git a/src/types/controllers/animeEpisodeSrcs.ts b/src/types/controllers/animeEpisodeSrcs.ts deleted file mode 100644 index 0e82539..0000000 --- a/src/types/controllers/animeEpisodeSrcs.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type AnimeServers } from "../anime.js"; - -export type AnimeEpisodeSrcsQueryParams = { - id?: string; - server?: AnimeServers; - category?: "sub" | "dub"; -}; diff --git a/src/types/controllers/animeEpisodes.ts b/src/types/controllers/animeEpisodes.ts deleted file mode 100644 index 481eb87..0000000 --- a/src/types/controllers/animeEpisodes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AnimeEpisodePathParams = { - animeId?: string; -}; diff --git a/src/types/controllers/animeGenre.ts b/src/types/controllers/animeGenre.ts deleted file mode 100644 index 038d15e..0000000 --- a/src/types/controllers/animeGenre.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type GenreAnimePathParams = { - name?: string; -}; - -export type GenreAnimeQueryParams = { - page?: string; -}; diff --git a/src/types/controllers/animeProducer.ts b/src/types/controllers/animeProducer.ts deleted file mode 100644 index 7f8a9a7..0000000 --- a/src/types/controllers/animeProducer.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type AnimeProducerPathParams = { - name?: string; -}; - -export type AnimeProducerQueryParams = { - page?: string; -}; diff --git a/src/types/controllers/animeSearch.ts b/src/types/controllers/animeSearch.ts deleted file mode 100644 index fccb243..0000000 --- a/src/types/controllers/animeSearch.ts +++ /dev/null @@ -1,20 +0,0 @@ -export type AnimeSearchQueryParams = { - q?: string; - page?: string; - type?: string; - status?: string; - rated?: string; - score?: string; - season?: string; - language?: string; - start_date?: string; - end_date?: string; - sort?: string; - genres?: string; -}; - -export type SearchFilters = Omit<AnimeSearchQueryParams, "q" | "page">; - -export type FilterKeys = Partial< - keyof Omit<SearchFilters, "start_date" | "end_date"> ->; diff --git a/src/types/controllers/animeSearchSuggestion.ts b/src/types/controllers/animeSearchSuggestion.ts deleted file mode 100644 index 491daa0..0000000 --- a/src/types/controllers/animeSearchSuggestion.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type AnimeSearchSuggestQueryParams = { - q?: string; -}; diff --git a/src/types/controllers/episodeServers.ts b/src/types/controllers/episodeServers.ts deleted file mode 100644 index d711a40..0000000 --- a/src/types/controllers/episodeServers.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type EpisodeServersQueryParams = { - episodeId?: string; -}; diff --git a/src/types/controllers/estimatedSchedule.ts b/src/types/controllers/estimatedSchedule.ts deleted file mode 100644 index e732aaa..0000000 --- a/src/types/controllers/estimatedSchedule.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type EstimatedScheduleQueryParams = { - date?: string; -}; diff --git a/src/types/controllers/index.ts b/src/types/controllers/index.ts deleted file mode 100644 index 14876e2..0000000 --- a/src/types/controllers/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type * from "./animeGenre.js"; -export type * from "./animeCategory.js"; -export type * from "./animeProducer.js"; -export type * from "./animeSearch.js"; -export type * from "./animeEpisodes.js"; -export type * from "./episodeServers.js"; -export type * from "./animeAboutInfo.js"; -export type * from "./animeEpisodeSrcs.js"; -export type * from "./estimatedSchedule.js"; -export type * from "./animeSearchSuggestion.js"; diff --git a/src/types/extractor.ts b/src/types/extractor.ts deleted file mode 100644 index 71c6be1..0000000 --- a/src/types/extractor.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Video { - url: string; - quality?: string; - isM3U8?: boolean; - size?: number; - [x: string]: unknown; -} - -export interface Subtitle { - id?: string; - url: string; - lang: string; -} - -export interface Intro { - start: number; - end: number; -} diff --git a/src/types/parsers/animeAboutInfo.ts b/src/types/parsers/animeAboutInfo.ts deleted file mode 100644 index 274e7c2..0000000 --- a/src/types/parsers/animeAboutInfo.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { - Season, - RelatedAnime, - RecommendedAnime, - AnimeGeneralAboutInfo, -} from "../anime.js"; -import { type HttpError } from "http-errors"; -import { type ScrapedAnimeSearchResult } from "./animeSearch.js"; - -export interface ScrapedAnimeAboutInfo - extends Pick<ScrapedAnimeSearchResult, "mostPopularAnimes"> { - anime: { - info: AnimeGeneralAboutInfo; - moreInfo: Record<string, string | string[]>; - }; - seasons: Array<Season>; - relatedAnimes: Array<RelatedAnime> | HttpError; - recommendedAnimes: Array<RecommendedAnime> | HttpError; -} diff --git a/src/types/parsers/animeCategory.ts b/src/types/parsers/animeCategory.ts deleted file mode 100644 index 89c56f0..0000000 --- a/src/types/parsers/animeCategory.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { HttpError } from "http-errors"; -import type { Anime, Top10Anime } from "../anime.js"; - -export interface ScrapedAnimeCategory { - animes: Array<Anime> | HttpError; - genres: Array<string>; - top10Animes: { - today: Array<Top10Anime> | HttpError; - week: Array<Top10Anime> | HttpError; - month: Array<Top10Anime> | HttpError; - }; - category: string; - totalPages: number; - currentPage: number; - hasNextPage: boolean; -} - -export type CommonAnimeScrapeTypes = - | "animes" - | "totalPages" - | "hasNextPage" - | "currentPage"; diff --git a/src/types/parsers/animeEpisodeSrcs.ts b/src/types/parsers/animeEpisodeSrcs.ts deleted file mode 100644 index ef58ed3..0000000 --- a/src/types/parsers/animeEpisodeSrcs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Intro, Subtitle, Video } from "../extractor.js"; - -export interface ScrapedAnimeEpisodesSources { - headers?: { - [k: string]: string; - }; - intro?: Intro; - subtitles?: Subtitle[]; - sources: Video[]; - download?: string; - embedURL?: string; -} diff --git a/src/types/parsers/animeEpisodes.ts b/src/types/parsers/animeEpisodes.ts deleted file mode 100644 index 3587573..0000000 --- a/src/types/parsers/animeEpisodes.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type AnimeEpisode } from "../anime.js"; - -export interface ScrapedAnimeEpisodes { - totalEpisodes: number; - episodes: Array<AnimeEpisode>; -} diff --git a/src/types/parsers/animeGenre.ts b/src/types/parsers/animeGenre.ts deleted file mode 100644 index 10f8b85..0000000 --- a/src/types/parsers/animeGenre.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { - ScrapedAnimeCategory, - CommonAnimeScrapeTypes, -} from "./animeCategory.js"; -import { type ScrapedHomePage } from "./homePage.js"; - -export interface ScrapedGenreAnime - extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes | "genres">, - Pick<ScrapedHomePage, "topAiringAnimes"> { - genreName: string; -} diff --git a/src/types/parsers/animeProducer.ts b/src/types/parsers/animeProducer.ts deleted file mode 100644 index bcd784e..0000000 --- a/src/types/parsers/animeProducer.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ScrapedHomePage } from "./homePage.js"; -import type { ScrapedAnimeCategory } from "./animeCategory.js"; - -export interface ScrapedProducerAnime - extends Omit<ScrapedAnimeCategory, "genres" | "category">, - Pick<ScrapedHomePage, "topAiringAnimes"> { - producerName: string; -} diff --git a/src/types/parsers/animeSearch.ts b/src/types/parsers/animeSearch.ts deleted file mode 100644 index 7e641ea..0000000 --- a/src/types/parsers/animeSearch.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { - ScrapedAnimeCategory, - CommonAnimeScrapeTypes, -} from "./animeCategory.js"; -import type { HttpError } from "http-errors"; -import type { MostPopularAnime } from "../anime.js"; -import type { SearchFilters } from "../controllers/animeSearch.js"; - -export interface ScrapedAnimeSearchResult - extends Pick<ScrapedAnimeCategory, CommonAnimeScrapeTypes> { - mostPopularAnimes: Array<MostPopularAnime> | HttpError; - searchQuery: string; - searchFilters: SearchFilters; -} diff --git a/src/types/parsers/animeSearchSuggestion.ts b/src/types/parsers/animeSearchSuggestion.ts deleted file mode 100644 index 4c262a4..0000000 --- a/src/types/parsers/animeSearchSuggestion.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { HttpError } from "http-errors"; -import type { AnimeSearchSuggestion } from "../anime.js"; - -export interface ScrapedAnimeSearchSuggestion { - suggestions: Array<AnimeSearchSuggestion> | HttpError; -} diff --git a/src/types/parsers/episodeServers.ts b/src/types/parsers/episodeServers.ts deleted file mode 100644 index a003149..0000000 --- a/src/types/parsers/episodeServers.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { SubEpisode, DubEpisode, RawEpisode } from "../anime.js"; - -export interface ScrapedEpisodeServers { - sub: SubEpisode[]; - dub: DubEpisode[]; - raw: RawEpisode[]; - episodeNo: number; - episodeId: string; -} diff --git a/src/types/parsers/estimatedSchedule.ts b/src/types/parsers/estimatedSchedule.ts deleted file mode 100644 index 881be86..0000000 --- a/src/types/parsers/estimatedSchedule.ts +++ /dev/null @@ -1,13 +0,0 @@ -type EstimatedSchedule = { - id: string | null; - time: string | null; - name: string | null; - jname: string | null; - airingTimestamp: number; - secondsUntilAiring: number; - episode: number; -}; - -export type ScrapedEstimatedSchedule = { - scheduledAnimes: Array<EstimatedSchedule>; -}; diff --git a/src/types/parsers/homePage.ts b/src/types/parsers/homePage.ts deleted file mode 100644 index 23f6284..0000000 --- a/src/types/parsers/homePage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { - TrendingAnime, - SpotlightAnime, - TopAiringAnime, - TopUpcomingAnime, - LatestEpisodeAnime, - MostFavoriteAnime, - MostPopularAnime, - LatestCompletedAnime, -} from "../anime.js"; -import type { HttpError } from "http-errors"; -import type { ScrapedAnimeCategory } from "./animeCategory.js"; - -export interface ScrapedHomePage - extends Pick<ScrapedAnimeCategory, "genres" | "top10Animes"> { - spotlightAnimes: Array<SpotlightAnime> | HttpError; - trendingAnimes: Array<TrendingAnime> | HttpError; - latestEpisodeAnimes: Array<LatestEpisodeAnime> | HttpError; - topUpcomingAnimes: Array<TopUpcomingAnime> | HttpError; - topAiringAnimes: Array<TopAiringAnime> | HttpError; - mostPopularAnimes: Array<MostPopularAnime> | HttpError; - mostFavoriteAnimes: Array<MostFavoriteAnime> | HttpError; - latestCompletedAnimes: Array<LatestCompletedAnime> | HttpError; -} diff --git a/src/types/parsers/index.ts b/src/types/parsers/index.ts deleted file mode 100644 index 057e2fc..0000000 --- a/src/types/parsers/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { ScrapedHomePage } from "./homePage.js"; -import type { ScrapedGenreAnime } from "./animeGenre.js"; -import type { ScrapedAnimeEpisodes } from "./animeEpisodes.js"; -import type { ScrapedAnimeCategory } from "./animeCategory.js"; -import type { ScrapedProducerAnime } from "./animeProducer.js"; -import type { ScrapedEpisodeServers } from "./episodeServers.js"; -import type { ScrapedAnimeAboutInfo } from "./animeAboutInfo.js"; -import type { ScrapedAnimeSearchResult } from "./animeSearch.js"; -import type { ScrapedEstimatedSchedule } from "./estimatedSchedule.js"; -import type { ScrapedAnimeEpisodesSources } from "./animeEpisodeSrcs.js"; -import type { ScrapedAnimeSearchSuggestion } from "./animeSearchSuggestion.js"; - -export type { - ScrapedHomePage, - ScrapedGenreAnime, - ScrapedAnimeEpisodes, - ScrapedProducerAnime, - ScrapedAnimeCategory, - ScrapedEpisodeServers, - ScrapedAnimeAboutInfo, - ScrapedAnimeSearchResult, - ScrapedEstimatedSchedule, - ScrapedAnimeEpisodesSources, - ScrapedAnimeSearchSuggestion, -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts deleted file mode 100644 index de7617f..0000000 --- a/src/utils/constants.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { config } from "dotenv"; - -config(); - -export const ACCEPT_ENCODING_HEADER = "gzip, deflate, br"; -export const USER_AGENT_HEADER = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4692.71 Safari/537.36"; -export const ACCEPT_HEADER = - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; - -// previously aniwatch.to || aniwatchtv.to -const DOMAIN = process.env.DOMAIN || "hianime.to"; - -export const SRC_BASE_URL = `https://${DOMAIN}` as const; -export const SRC_AJAX_URL = `${SRC_BASE_URL}/ajax` as const; -export const SRC_HOME_URL = `${SRC_BASE_URL}/home` as const; -export const SRC_SEARCH_URL = `${SRC_BASE_URL}/search` as const; - -// <SearchPageFilters> -export const genresIdMap: Record<string, number> = { - action: 1, - adventure: 2, - cars: 3, - comedy: 4, - dementia: 5, - demons: 6, - drama: 8, - ecchi: 9, - fantasy: 10, - game: 11, - harem: 35, - historical: 13, - horror: 14, - isekai: 44, - josei: 43, - kids: 15, - magic: 16, - "martial-arts": 17, - mecha: 18, - military: 38, - music: 19, - mystery: 7, - parody: 20, - police: 39, - psychological: 40, - romance: 22, - samurai: 21, - school: 23, - "sci-fi": 24, - seinen: 42, - shoujo: 25, - "shoujo-ai": 26, - shounen: 27, - "shounen-ai": 28, - "slice-of-life": 36, - space: 29, - sports: 30, - "super-power": 31, - supernatural: 37, - thriller: 41, - vampire: 32, -} as const; - -export const typeIdMap: Record<string, number> = { - all: 0, - movie: 1, - tv: 2, - ova: 3, - ona: 4, - special: 5, - music: 6, -} as const; - -export const statusIdMap: Record<string, number> = { - all: 0, - "finished-airing": 1, - "currently-airing": 2, - "not-yet-aired": 3, -} as const; - -export const ratedIdMap: Record<string, number> = { - all: 0, - g: 1, - pg: 2, - "pg-13": 3, - r: 4, - "r+": 5, - rx: 6, -} as const; - -export const scoreIdMap: Record<string, number> = { - all: 0, - appalling: 1, - horrible: 2, - "very-bad": 3, - bad: 4, - average: 5, - fine: 6, - good: 7, - "very-good": 8, - great: 9, - masterpiece: 10, -} as const; - -export const seasonIdMap: Record<string, number> = { - all: 0, - spring: 1, - summer: 2, - fall: 3, - winter: 4, -} as const; - -export const languageIdMap: Record<string, number> = { - all: 0, - sub: 1, - dub: 2, - "sub-&-dub": 3, -} as const; - -export const sortIdMap: Record<string, string> = { - default: "default", - "recently-added": "recently_added", - "recently-updated": "recently_updated", - score: "score", - "name-a-z": "name_az", - "released-date": "released_date", - "most-watched": "most_watched", -} as const; -// </SearchPageFilters> diff --git a/src/utils/index.ts b/src/utils/index.ts deleted file mode 100644 index b514c85..0000000 --- a/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./methods.js"; -export * from "./constants.js"; diff --git a/src/utils/methods.ts b/src/utils/methods.ts deleted file mode 100644 index 6aed9f7..0000000 --- a/src/utils/methods.ts +++ /dev/null @@ -1,297 +0,0 @@ -import type { - Anime, - Top10Anime, - MostPopularAnime, - Top10AnimeTimePeriod, -} from "../types/anime.js"; -import type { CheerioAPI, SelectorType } from "cheerio"; -import { - genresIdMap, - languageIdMap, - ratedIdMap, - scoreIdMap, - seasonIdMap, - sortIdMap, - statusIdMap, - typeIdMap, -} from "./constants.js"; -import { type FilterKeys } from "../types/controllers/animeSearch.js"; -import createHttpError, { HttpError } from "http-errors"; - -export const extractAnimes = ( - $: CheerioAPI, - selector: SelectorType -): Array<Anime> | HttpError => { - try { - const animes: Array<Anime> = []; - - $(selector).each((i, el) => { - const animeId = - $(el) - .find(".film-detail .film-name .dynamic-name") - ?.attr("href") - ?.slice(1) - .split("?ref=search")[0] || null; - - animes.push({ - id: animeId, - name: $(el) - .find(".film-detail .film-name .dynamic-name") - ?.text() - ?.trim(), - jname: - $(el) - .find(".film-detail .film-name .dynamic-name") - ?.attr("data-jname") - ?.trim() || null, - poster: - $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim() || null, - duration: $(el) - .find(".film-detail .fd-infor .fdi-item.fdi-duration") - ?.text() - ?.trim(), - type: $(el) - .find(".film-detail .fd-infor .fdi-item:nth-of-type(1)") - ?.text() - ?.trim(), - rating: $(el).find(".film-poster .tick-rate")?.text()?.trim() || null, - episodes: { - sub: - Number( - $(el) - .find(".film-poster .tick-sub") - ?.text() - ?.trim() - .split(" ") - .pop() - ) || null, - dub: - Number( - $(el) - .find(".film-poster .tick-dub") - ?.text() - ?.trim() - .split(" ") - .pop() - ) || null, - }, - }); - }); - - return animes; - } catch (err: any) { - throw createHttpError.InternalServerError( - err?.message || "Something went wrong" - ); - } -}; - -export const extractTop10Animes = ( - $: CheerioAPI, - period: Top10AnimeTimePeriod -): Array<Top10Anime> | HttpError => { - try { - const animes: Array<Top10Anime> = []; - const selector = `#top-viewed-${period} ul li`; - - $(selector).each((i, el) => { - animes.push({ - id: - $(el) - .find(".film-detail .dynamic-name") - ?.attr("href") - ?.slice(1) - .trim() || null, - rank: Number($(el).find(".film-number span")?.text()?.trim()) || null, - name: $(el).find(".film-detail .dynamic-name")?.text()?.trim() || null, - jname: - $(el) - .find(".film-detail .dynamic-name") - ?.attr("data-jname") - ?.trim() || null, - poster: - $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim() || null, - episodes: { - sub: - Number( - $(el) - .find(".film-detail .fd-infor .tick-item.tick-sub") - ?.text() - ?.trim() - ) || null, - dub: - Number( - $(el) - .find(".film-detail .fd-infor .tick-item.tick-dub") - ?.text() - ?.trim() - ) || null, - }, - }); - }); - - return animes; - } catch (err: any) { - throw createHttpError.InternalServerError( - err?.message || "Something went wrong" - ); - } -}; - -export const extractMostPopularAnimes = ( - $: CheerioAPI, - selector: SelectorType -): Array<MostPopularAnime> | HttpError => { - try { - const animes: Array<MostPopularAnime> = []; - - $(selector).each((i, el) => { - animes.push({ - id: - $(el) - .find(".film-detail .dynamic-name") - ?.attr("href") - ?.slice(1) - .trim() || null, - name: $(el).find(".film-detail .dynamic-name")?.text()?.trim() || null, - jname: - $(el) - .find(".film-detail .film-name .dynamic-name") - .attr("data-jname") - ?.trim() || null, - poster: - $(el) - .find(".film-poster .film-poster-img") - ?.attr("data-src") - ?.trim() || null, - episodes: { - sub: - Number($(el)?.find(".fd-infor .tick .tick-sub")?.text()?.trim()) || - null, - dub: - Number($(el)?.find(".fd-infor .tick .tick-dub")?.text()?.trim()) || - null, - }, - type: - $(el) - ?.find(".fd-infor .tick") - ?.text() - ?.trim() - ?.replace(/[\s\n]+/g, " ") - ?.split(" ") - ?.pop() || null, - }); - }); - - return animes; - } catch (err: any) { - throw createHttpError.InternalServerError( - err?.message || "Something went wrong" - ); - } -}; - -export function retrieveServerId( - $: CheerioAPI, - index: number, - category: "sub" | "dub" | "raw" -) { - return ( - $(`.ps_-block.ps_-block-sub.servers-${category} > .ps__-list .server-item`) - ?.map((_, el) => - $(el).attr("data-server-id") == `${index}` ? $(el) : null - ) - ?.get()[0] - ?.attr("data-id") || null - ); -} - -function getGenresFilterVal(genreNames: string[]): string | undefined { - if (genreNames.length < 1) { - return undefined; - } - return genreNames.map((name) => genresIdMap[name]).join(","); -} - -export function getSearchFilterValue( - key: FilterKeys, - rawValue: string -): string | undefined { - rawValue = rawValue.trim(); - if (!rawValue) return undefined; - - switch (key) { - case "genres": { - return getGenresFilterVal(rawValue.split(",")); - } - case "type": { - const val = typeIdMap[rawValue] ?? 0; - return val === 0 ? undefined : `${val}`; - } - case "status": { - const val = statusIdMap[rawValue] ?? 0; - return val === 0 ? undefined : `${val}`; - } - case "rated": { - const val = ratedIdMap[rawValue] ?? 0; - return val === 0 ? undefined : `${val}`; - } - case "score": { - const val = scoreIdMap[rawValue] ?? 0; - return val === 0 ? undefined : `${val}`; - } - case "season": { - const val = seasonIdMap[rawValue] ?? 0; - return val === 0 ? undefined : `${val}`; - } - case "language": { - const val = languageIdMap[rawValue] ?? 0; - return val === 0 ? undefined : `${val}`; - } - case "sort": { - return sortIdMap[rawValue] ?? undefined; - } - default: - return undefined; - } -} - -// this fn tackles both start_date and end_date -export function getSearchDateFilterValue( - isStartDate: boolean, - rawValue: string -): string[] | undefined { - rawValue = rawValue.trim(); - if (!rawValue) return undefined; - - const dateRegex = /^\d{4}-([0-9]|1[0-2])-([0-9]|[12][0-9]|3[01])$/; - const dateCategory = isStartDate ? "s" : "e"; - const [year, month, date] = rawValue.split("-"); - - if (!dateRegex.test(rawValue)) { - return undefined; - } - - // sample return -> [sy=2023, sm=10, sd=11] - return [ - Number(year) > 0 ? `${dateCategory}y=${year}` : "", - Number(month) > 0 ? `${dateCategory}m=${month}` : "", - Number(date) > 0 ? `${dateCategory}d=${date}` : "", - ].filter((d) => Boolean(d)); -} - -export function substringAfter(str: string, toFind: string) { - const index = str.indexOf(toFind); - return index == -1 ? "" : str.substring(index + toFind.length); -} - -export function substringBefore(str: string, toFind: string) { - const index = str.indexOf(toFind); - return index == -1 ? "" : str.substring(0, index); -} diff --git a/test/animeAboutInfo.test.ts b/test/animeAboutInfo.test.ts index 2cdc0cb..82fe97d 100644 --- a/test/animeAboutInfo.test.ts +++ b/test/animeAboutInfo.test.ts @@ -1,8 +1,12 @@ +import { HiAnime } from "aniwatch"; import { expect, test } from "vitest"; -import { scrapeAnimeAboutInfo } from "../src/parsers/index.js"; -test("returns information about an anime", async () => { - const data = await scrapeAnimeAboutInfo("steinsgate-3"); +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([]); diff --git a/test/animeCategory.test.ts b/test/animeCategory.test.ts index 55ad873..045b0f9 100644 --- a/test/animeCategory.test.ts +++ b/test/animeCategory.test.ts @@ -1,8 +1,12 @@ import { expect, test } from "vitest"; -import { scrapeAnimeCategory } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns animes belonging to a category", async () => { - const data = await scrapeAnimeCategory("subbed-anime"); +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([]); diff --git a/test/animeEpisodeSrcs.test.ts b/test/animeEpisodeSrcs.test.ts index 1c6f160..d8db0ed 100644 --- a/test/animeEpisodeSrcs.test.ts +++ b/test/animeEpisodeSrcs.test.ts @@ -1,13 +1,18 @@ import { expect, test } from "vitest"; -import { scrapeAnimeEpisodeSources } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns anime episode streaming link(s)", async () => { - const data = await scrapeAnimeEpisodeSources( - "steinsgate-3?ep=230", - "hd-1", - "sub" +const animeEpisodeId = "steinsgate-3?ep=230"; +const server = "hd-1"; +const category = "sub"; + +// npx vitest run animeEpisodeSrcs.test.ts +test(`GET /api/v2/hianime/episode/sources?animeEpisodeId=${animeEpisodeId}&server=${server}&category=${category}`, async () => { + const hianime = new HiAnime.Scraper(); + const data = await hianime.getEpisodeSources( + animeEpisodeId, + server, + category ); expect(data.sources).not.toEqual([]); - // expect(data) }); diff --git a/test/animeEpisodes.test.ts b/test/animeEpisodes.test.ts index f727056..fbe75ae 100644 --- a/test/animeEpisodes.test.ts +++ b/test/animeEpisodes.test.ts @@ -1,8 +1,12 @@ +import { HiAnime } from "aniwatch"; import { expect, test } from "vitest"; -import { scrapeAnimeEpisodes } from "../src/parsers/index.js"; -test("returns episodes info of an anime", async () => { - const data = await scrapeAnimeEpisodes("steinsgate-3"); +const animeId = "steinsgate-3"; + +// npx vitest run animeEpisodes.test.ts +test(`GET /api/v2/hianime/anime/${animeId}/episodes`, async () => { + const hianime = new HiAnime.Scraper(); + const data = await hianime.getEpisodes(animeId); expect(data.totalEpisodes).not.toEqual(0); expect(data.episodes).not.toEqual([]); diff --git a/test/animeGenre.test.ts b/test/animeGenre.test.ts index 9e6610a..f08ad1b 100644 --- a/test/animeGenre.test.ts +++ b/test/animeGenre.test.ts @@ -1,8 +1,14 @@ import { expect, test } from "vitest"; -import { scrapeGenreAnime } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns animes belonging to a genre", async () => { - const data = await scrapeGenreAnime("shounen", 2); +const genreName = "shounen"; +const page = 2; + +// npx vitest run animeGenre.test.ts +test(`GET /api/v2/hianime/genre/${genreName}?page=${page}`, async () => { + const hianime = new HiAnime.Scraper(); + + const data = await hianime.getGenreAnime(genreName, page); expect(data.animes).not.toEqual([]); expect(data.genres).not.toEqual([]); diff --git a/test/animeProducer.test.ts b/test/animeProducer.test.ts index ae17c83..53f0d28 100644 --- a/test/animeProducer.test.ts +++ b/test/animeProducer.test.ts @@ -1,8 +1,13 @@ import { expect, test } from "vitest"; -import { scrapeProducerAnimes } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns animes produced by a producer", async () => { - const data = await scrapeProducerAnimes("toei-animation", 2); +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([]); diff --git a/test/animeSearch.test.ts b/test/animeSearch.test.ts index efd2641..dac66cb 100644 --- a/test/animeSearch.test.ts +++ b/test/animeSearch.test.ts @@ -1,8 +1,16 @@ import { expect, test } from "vitest"; -import { scrapeAnimeSearch } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns animes related to search query", async () => { - const data = await scrapeAnimeSearch("monster", 2); +const query = "monster"; +const page = 1; +const filter: HiAnime.SearchFilters = { + genres: "seinen,psychological", +}; + +// npx vitest run animeSearch.test.ts +test(`GET /api/v2/hianime/search?q=${query}&page=${page}&genres=${filter.genres}`, async () => { + const hianime = new HiAnime.Scraper(); + const data = await hianime.search(query, page, filter); expect(data.animes).not.toEqual([]); expect(data.mostPopularAnimes).not.toEqual([]); diff --git a/test/animeSearchSuggestion.test.ts b/test/animeSearchSuggestion.test.ts index f3a875a..5957528 100644 --- a/test/animeSearchSuggestion.test.ts +++ b/test/animeSearchSuggestion.test.ts @@ -1,8 +1,12 @@ import { expect, test } from "vitest"; -import { scrapeAnimeSearchSuggestion } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns animes search suggestions related to search query", async () => { - const data = await scrapeAnimeSearchSuggestion("one piece"); +const query = "one piece"; + +// npx vitest run animeSearchSuggestion.test.ts +test(`GET /api/v2/hianime/search/suggestion?q=${query}`, async () => { + const hianime = new HiAnime.Scraper(); + const data = await hianime.searchSuggestions(query); expect(data.suggestions).not.toEqual([]); }); diff --git a/test/episodeServers.test.ts b/test/episodeServers.test.ts index 73cc5ca..c044e6c 100644 --- a/test/episodeServers.test.ts +++ b/test/episodeServers.test.ts @@ -1,8 +1,12 @@ import { expect, test } from "vitest"; -import { scrapeEpisodeServers } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns episode source servers", async () => { - const data = await scrapeEpisodeServers("steinsgate-0-92?ep=2055"); +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); diff --git a/test/estimatedSchedule.test.ts b/test/estimatedSchedule.test.ts index 40b6956..1e8e086 100644 --- a/test/estimatedSchedule.test.ts +++ b/test/estimatedSchedule.test.ts @@ -1,15 +1,17 @@ import { expect, test } from "vitest"; -import { scrapeEstimatedSchedule } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -function padZero(num: number) { - return num < 10 ? `0${num}` : num.toString(); -} +const padZero = (num: number) => (num < 10 ? `0${num}` : num.toString()); -test("returns estimated schedule anime release", async () => { - const d = new Date(); - const data = await scrapeEstimatedSchedule( - `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero(d.getDate())}` - ); +const d = new Date(); +const date = `${d.getFullYear()}-${padZero(d.getMonth() + 1)}-${padZero( + d.getDate() +)}`; + +// npx vitest run estimatedSchedule.test.ts +test(`GET /api/v2/hianime/schedule?date=${date}`, async () => { + const hianime = new HiAnime.Scraper(); + const data = await hianime.getEstimatedSchedule(date); expect(data.scheduledAnimes).not.toEqual([]); }); diff --git a/test/homePage.test.ts b/test/homePage.test.ts index 25b409c..a5f9e8f 100644 --- a/test/homePage.test.ts +++ b/test/homePage.test.ts @@ -1,8 +1,9 @@ import { expect, test } from "vitest"; -import { scrapeHomePage } from "../src/parsers/index.js"; +import { HiAnime } from "aniwatch"; -test("returns anime information present in homepage", async () => { - const data = await scrapeHomePage(); +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([]); diff --git a/tsconfig.json b/tsconfig.json index 278db51..b958b91 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "esModuleInterop": true, "skipLibCheck": true, - "target": "ES2022", + "target": "ES2023", "verbatimModuleSyntax": true, "allowJs": true, "resolveJsonModule": true, @@ -13,6 +13,8 @@ // "moduleResolution": "NodeNext", "module": "NodeNext", + "noUnusedLocals": true, + "noUnusedParameters": true, "outDir": "./dist", "rootDir": "./", "sourceMap": true, @@ -21,18 +23,8 @@ "removeComments": true, "forceConsistentCasingInFileNames": true, "strictFunctionTypes": true, - "lib": [ - "ES2022" - ] + "lib": ["ES2023"] }, - "include": [ - "./src", - "./api" - ], - "ts-node": { - "esm": true - }, - "exclude": [ - "node_modules", - ], -}
\ No newline at end of file + "include": ["./src", "./api"], + "exclude": ["node_modules"] +} |
