diff --git a/README.md b/README.md index 3b3da9d56..acb22a6c8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-

Consumet Extentions

+

Consumet Extensions

Consumet Extensions is a Node library which provides high-level APIs to get information about several entertainment mediums like books, movies, comics, anime, manga, etc. @@ -32,6 +32,7 @@ Consumet Extensions is a Node library which provides high-level APIs to get info - [Provider Request](#provider-request) - [Contributing](#contributing) - [Support](#support) +- [Contributors](#contributors) - [License](#license) ## Quick Start @@ -50,33 +51,26 @@ yarn add @consumet/extensions ```ts import { BOOKS } from "@consumet/extensions" - -const main = async () => { - // Create a new instance of the Libgen provider - const books = new BOOKS.Libgen(); - // Search for a book. In this case, "Pride and Prejudice" - const data = await books.search('pride and prejudice'); - // loop through the results and print the title - for (let v of data) { - console.log(v.title); - } -}; - -main(); +// Create a new instance of the Libgen provider +const books = new BOOKS.Libgen(); +// Search for a book. In this case, "Pride and Prejudice" +const data = books.search('pride and prejudice').then(data => { + // print results + console.log(data) +}) ``` **Exmaple** - searching for anime using the gogoanime provider. ```ts import { ANIME } from "@consumet/extensions" -const main = async () => { - // Create a new instance of the Gogoanime provider - const gogoanime = new ANIME.Gogoanime(); - // Search for an anime. In this case, "One Piece" - const results = await gogoanime.search("One Piece"); - // print the results - console.log(results); -} +// Create a new instance of the Gogoanime provider +const gogoanime = new ANIME.Gogoanime(); +// Search for an anime. In this case, "One Piece" +const results = gogoanime.search("One Piece").then(data => { + // print results + console.log(data); +}) ``` Do you want to know more? Head to the [`Getting Started`](https://github.com/consumet/extensions/tree/master/docs/guides/getting-started.md). @@ -99,7 +93,7 @@ Do you want to know more? Head to the [`Getting Started`](https://github.com/con - [Discord Server](https://discord.gg/qTPfvMxzNH) - Join our discord server and chat with the maintainers. ## Provider Request -Make a new [issue](https://github.com/consumet/extensions/issues/new?assignees=&labels=Provider+Request&template=provider-request.yml) with the name of the provider on the title, as well as a link to the provider in the body paragraph. +Make a new [issue](https://github.com/consumet/extensions/issues/new?assignees=&labels=provider+request&template=provider-request.yml) with the name of the provider on the title, as well as a link to the provider in the body paragraph. ## Contributing Check out [contributing guide](https://github.com/consumet/extensions/blob/master/docs/guides/contributing.md) to get an overview of Consumet Extensions development. @@ -107,5 +101,10 @@ Check out [contributing guide](https://github.com/consumet/extensions/blob/maste ## Support Please join the [discord server](https://discord.gg/qTPfvMxzNH) to ask questions, get help, or report issues. +## Contributors +Thanks to the following people who have contributed to this repo: + +[![](https://avatars.githubusercontent.com/u/57333995?s=50)](https://github.com/riimuru) [![](https://avatars.githubusercontent.com/u/80477926?s=50)](https://github.com/prince-ao) + ## License Licensed under [MIT](./LICENSE). diff --git a/docs/guides/anime.md b/docs/guides/anime.md index af5f9c885..637f997d3 100644 --- a/docs/guides/anime.md +++ b/docs/guides/anime.md @@ -2,9 +2,7 @@

ANIME

-`ANIME` is a category provider, which provides a list of anime providers. Each anime provider must be a subclass of the [`AnimeParser`](https://github.com/consumet/extensions/blob/master/src/models/anime-parser.ts) class. - -By using `ANIME` category you can interact with the anime providers. And have access to the anime providers methods. +By using `ANIME` category you can interact with the anime providers. And get access to the anime providers methods. Which allows you to search for anime, get the anime information, get the anime episodes with streaming links. ```ts // ESM @@ -17,10 +15,7 @@ const animeProvider = ANIME.(); ## Anime Providers List This list is in alphabetical order. (except the sub bullet points) +- [AnimePahe](../providers/animepahe.md) - [Gogoanime](../providers/gogoanime.md) - - [search](../providers/gogoanime.md#search) - - [fetchAnimeInfo](../providers/gogoanime.md#fetchanimeinfo) - - [fetchEpisodeSources](../providers/gogoanime.md#fetchepisodesources) - - [fetchEpisodeServers](../providers/gogoanime.md#fetchepisodeservers)

(back to table of contents)

\ No newline at end of file diff --git a/docs/guides/books.md b/docs/guides/books.md index 12efb6481..63f35c45d 100644 --- a/docs/guides/books.md +++ b/docs/guides/books.md @@ -2,9 +2,7 @@

BOOKS

-`BOOKS` is a category provider, which provides a list of anime providers. Each anime provider must be a subclass of the [`BookParser`](https://github.com/consumet/extensions/blob/master/src/models/book-parsers.ts) class. - -By using `BOOKS` category you can interact with the book providers. And have access to the book providers methods. +By using `BOOKS` category you can interact with the book providers. And have access to the book providers methods. Which allows you to search for books, get the book information, get the book pdf/epub links. ```ts // ESM diff --git a/docs/guides/comics.md b/docs/guides/comics.md index c326a03aa..29d56e93f 100644 --- a/docs/guides/comics.md +++ b/docs/guides/comics.md @@ -2,9 +2,7 @@

COMICS

-`COMICS` is a category provider, which provides a list of anime providers. Each anime provider must be a subclass of the [`ComicParser`](https://github.com/consumet/extensions/blob/master/src/models/comic-parsers.ts) class. - -By using `COMICS` category you can interact with the book providers. And have access to the comic providers methods. +By using `COMICS` category you can interact with the book providers. And have access to the comic providers methods. Which allows you to search for comics, get the comic information, get the comic chapters with images to read. ```ts // ESM diff --git a/docs/guides/light-novels.md b/docs/guides/light-novels.md index 3442fdf52..a9e05534b 100644 --- a/docs/guides/light-novels.md +++ b/docs/guides/light-novels.md @@ -2,9 +2,7 @@

LIGHT_NOVELS

-`LIGHT_NOVELS` is a category provider, which provides a list of light novels providers. Each light novel provider must be a subclass of the [`LightNovelParser`](https://github.com/consumet/extensions/blob/master/src/models/lightnovel-parser.ts) class. - -By using `LIGHT_NOVELS` category you can interact with the light novel providers. And have access to the light novel providers methods. +By using `LIGHT_NOVELS` category you can interact with the light novel providers. And have access to the light novel providers methods. Which allows you to search for light novels, get the light novel information, get the light novel chapters with text content to read. ```ts // ESM @@ -19,9 +17,6 @@ const lightnovelProvider = LIGHT_NOVELS.(); This list is in alphabetical order. (except the sub bullet points) - [ReadLightNovels](../providers/readlightnovels.md) - - [search](../providers/readlightnovels.md#search) - - [fetchLightNovelInfo](../providers/readlightnovels.md#fetchlightnovelinfo) - - [fetchChapterContent](../providers/readlightnovels.md#fetchchaptercontent)

(back to table of contents)

\ No newline at end of file diff --git a/docs/guides/manga.md b/docs/guides/manga.md index 09f11699e..30813f537 100644 --- a/docs/guides/manga.md +++ b/docs/guides/manga.md @@ -2,9 +2,7 @@

MANGA

-`MANGA` is a category provider, which provides a list of manga providers. Each manga provider must be a subclass of the [`MangaParser`](https://github.com/consumet/extensions/blob/master/src/models/manga-parser.ts) class. - -By using `MANGA` category you can interact with the manga providers. And have access to the manga providers methods. +By using `MANGA` category you can interact with the manga providers. And have access to the manga providers methods. Which allows you to search for manga, get the manga information, get the manga chapters with images to read. ```ts // ESM @@ -18,9 +16,6 @@ const mangaProvider = MANGA.(); This list is in alphabetical order. (except the sub bullet points) - [MangaDex](../providers/mangadex.md) - - [search](../providers/mangadex.md#search) - - [fetchMangaInfo](../providers/mangadex.md#fetchmangainfo) - - [fetchChapterPages](../providers/mangadex.md#fetchchapterpages)

(back to table of contents)

\ No newline at end of file diff --git a/docs/guides/movies.md b/docs/guides/movies.md index e86c8d74d..7288fbc37 100644 --- a/docs/guides/movies.md +++ b/docs/guides/movies.md @@ -2,9 +2,7 @@

MOVIES

-`MOVIES` is a category provider, which provides a list of movies/tv series providers. Each movie/tv series provider must be a subclass of the [`MovieParser`](https://github.com/consumet/extensions/blob/master/src/models/movie-parser.ts) class. - -By using `MOVIES` category you can interact with the movie providers. And have access to the movie providers methods. +By using `MOVIES` category you can interact with the movie providers. And have access to the movie providers methods. Which allows you to search for movies and shows, get the movie/tv series information, get the movie/tv series episodes with streaming links. ```ts // ESM @@ -18,9 +16,5 @@ const movieProvider = MOVIES.(); This list is in alphabetical order. (except the sub bullet points) - [FlixHQ](../providers/flixhq.md) - - [search](../providers/flixhq.md#search) - - [fetchMediaInfo](../providers/flixhq.md#fetchmediainfo) - - [fetchEpisodeSources](../providers/flixhq.md#fetchepisodesources) - - [fetchEpisodeServers](../providers/flixhq.md#fetchepisodeservers)

(back to table of contents)

\ No newline at end of file diff --git a/docs/providers/animepahe.md b/docs/providers/animepahe.md new file mode 100644 index 000000000..6673df344 --- /dev/null +++ b/docs/providers/animepahe.md @@ -0,0 +1,147 @@ +

AnimePahe

+ +```ts +const animepahe = new ANIME.AnimePahe(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search +> Note: This method is a subclass of the [`BaseParser`](https://github.com/consumet/extensions/blob/master/src/models/base-parser.ts) class. meaning it is available across most categories. + + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------ | +| query | `string` | query to search for. (*In this case, We're searching for `Overlord IV`*) | + +```ts +animepahe.search("Overlord IV").then(data => { + console.log(data); +} +``` + +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ +output: +```js +{ + results: [ + { + id: 'adb84358-8fec-fe80-1dc5-ad6218421dc1', + title: 'Overlord IV', + image: 'https://i.animepahe.com/posters/cb77e1e2a76b985a7c9d9b90a497fee65d89fa9c41d0e9e6fab4608d10313ddf.jpg', + rating: 8.3, + releaseDate: 2022, + type: 'TV' + }, + { + id: 'a0d776d3-48d2-5487-971d-f5d8dada5c42', + title: 'Overlord', + image: 'https://i.animepahe.com/posters/e78bf21dfd4e382dbc985501edb0f57bda7d5305b87863fe8991a5e658c9c1a8.jpg', + rating: 7.92, + releaseDate: 2015, + type: 'TV' + }, + {...} + ... + ] +} +``` + +### fetchAnimeInfo + +

Parameters

+ +| Parameter | Type | Description | +| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| id | `string` | takes anime id as a parameter. (*anime id can be found in the anime search results or anime info object*) | +| episodePage (optional) | `number` | takes episode page number as a parameter. default: -1 to get all episodes at once (*episodePages can be found in the anime info object*) | + + +```ts +animepahe.fetchAnimeInfo("adb84358-8fec-fe80-1dc5-ad6218421dc1").then(data => { + console.log(data); +} +``` + +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ +output: +```js +{ + id: 'adb84358-8fec-fe80-1dc5-ad6218421dc1', + title: 'Overlord IV', + image: 'https://i.animepahe.com/posters/cb77e1e2a76b985a7c9d9b90a497fee65d89fa9c41d0e9e6fab4608d10313ddf.jpg', + cover: 'https://i.animepahe.com/covers/cover_default3.jpg', + description: 'Fourth season of Overlord.', + genres: [ 'Action', 'Fantasy', 'Supernatural' ], + status: 'Ongoing', + type: 'TV', + releaseDate: 'Jul 05, 2022', + aired: 'Jul 05, 2022 to ?', + studios: [ 'Madhouse' ], + totalEpisodes: NaN, // NaN means that the anime is ongoing. + episodes: [ + { + id: 'c673b4d6cedf5e4cd1900d30d61ee2130e23a74e58f4401a85f21a4e95c94f73', + number: 1, + title: '', + image: 'https://i.animepahe.com/snapshots/8b3499c66e59e4c266485b54b78ad8469a520d9957dbe5a117f8d0934a93817a.jpg', + duration: '00:23:40' + } + ], + episodePages: 1 // 1 means that there is only one page of episodes. +} +``` + +### fetchEpisodeSources + +

Parameters

+ +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------------------------------------- | +| episodeId | `string` | takes episode id as a parameter. (*episode id can be found in the anime info object*) | + + +In this example, we're getting the sources for the first episode of Overlord IV. +```ts +animepahe.fetchEpisodeSources("c673b4d6cedf5e4cd1900d30d61ee2130e23a74e58f4401a85f21a4e95c94f73").then(data => { + console.log(data); +} +``` + +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ +output: +```js +{ + headers: { Referer: 'https://kwik.cx/' }, + sources: [ + { + url: 'https://na-191.files.nextcdn.org/hls/01/b49063a1225cf4350deb46d79b42a7572e323274d1c9d63f3b067cc4df09986a/uwu.m3u8', + isM3U8: true, + quality: '360', + size: 44617958 + }, + { + url: 'https://na-191.files.nextcdn.org/hls/01/c32da1b1975a5106dcee7e7182219f9b4dbef836fb782d7939003a8cde8f057f/uwu.m3u8', + isM3U8: true, + quality: '720', + size: 78630133 + }, + { + url: 'https://na-191.files.nextcdn.org/hls/01/b85d4450908232aa32b71bc67c80e8aedcc4f32a282e5df9ad82e4662786e9d8/uwu.m3u8', + isM3U8: true, + quality: '1080', + size: 118025148 + } + ] +} +``` + +Make sure to check the `headers` property of the returned object. It contains the referer header, which is needed to bypass the 403 error and allow you to stream the video without any issues. + +

(back to anime providers list)

\ No newline at end of file diff --git a/docs/providers/flixhq.md b/docs/providers/flixhq.md index e4aa64af0..12eed2e62 100644 --- a/docs/providers/flixhq.md +++ b/docs/providers/flixhq.md @@ -28,7 +28,7 @@ flixhq.search("Vincenzo").then(data => { } ``` -returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*) +returns a promise which resolves into an array of movies/tv series. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L233-L241)*)\ output: ```js { @@ -62,7 +62,7 @@ flixhq.fetchMediaInfo("tv/watch-vincenzo-67955").then(data => { } ``` -returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L243-L254)*) +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L243-L254)*)\ output: ```js { @@ -119,7 +119,7 @@ flixhq.fetchEpisodeSources("1167571", "tv/watch-vincenzo-67955").then(data => { console.log(data); } ``` -returns a promise which resolves into an array of episode sources and subtitles. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*) +returns a promise which resolves into an array of episode sources and subtitles. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ output: ```js { @@ -159,7 +159,7 @@ flixhq.fetchEpisodeServers('1167571', 'tv/watch-vincenzo-67955').then(data => { console.log(data); } ``` -returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L54-L57)*) +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L54-L57)*)\ output: ```js [ diff --git a/docs/providers/gogoanime.md b/docs/providers/gogoanime.md index 7f316bc5a..1b727fa0b 100644 --- a/docs/providers/gogoanime.md +++ b/docs/providers/gogoanime.md @@ -7,6 +7,8 @@ const gogoanime = new ANIME.Gogoanime();

Methods

- [search](#search) +- [fetchRecentEpisodes](#fetchrecentepisodes) +- [fetchTopAiring](#fetchtopairing) - [fetchAnimeInfo](#fetchanimeinfo) - [fetchEpisodeSources](#fetchepisodesources) - [fetchEpisodeServers](#fetchepisodeservers) @@ -28,7 +30,7 @@ gogoanime.search("One Piece").then(data => { } ``` -returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*) +returns a promise which resolves into an array of anime. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L13-L26)*)\ output: ```js { @@ -57,6 +59,76 @@ output: } ``` +### fetchRecentEpisodes + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| page (optional) | `number` | page number (default: 1) | +| type (optional) | `string` | type of anime (default: `1`). `1`: Japanese with subtitles, `2`: english/dub with no subtitles, `3`: chinese with english subtitles | + +```ts +gogoanime.fetchRecentEpisodes().then(data => { + console.log(data); +} +``` + +output: +```js +{ + currentPage: 1, // current page + hasNextPage: true, // if there is a next page + results: [ + { + id: 'hellsing', + episodeId: 'hellsing-episode-13', + episodeNumber: 13, + title: 'Hellsing', + image: 'https://gogocdn.net/images/anime/H/hellsing.jpg', + url: 'https://gogoanime.gg//hellsing-episode-13' + }, + {...} + ... + ] +} +``` + +### fetchTopAiring + +return top airing anime list. + +

Parameters

+ +| Parameter | Type | Description | +| --------------- | -------- | ------------------------ | +| page (optional) | `number` | page number (default: 1) | + +```ts +gogoanime.fetchTopAiring().then(data => { + console.log(data); +} +``` + +output: +```js +{ + currentPage: 1, + hasNextPage: true, + results: [ + { + id: 'overlord-iv', + title: 'Overlord IV', + image: 'https://gogocdn.net/cover/overlord-iv.png', + url: 'https://gogoanime.gg/category/overlord-iv', + genres: [ 'Action', 'Fantasy', 'Game', 'Magic', 'Supernatural' ] + } + {...} + ... + ] +} +``` + ### fetchAnimeInfo

Parameters

@@ -71,7 +143,7 @@ gogoanime.fetchAnimeInfo("one-piece").then(data => { } ``` -returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*) +returns a promise which resolves into an anime info object (including the episodes). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L28-L42)*)\ output: ```js { @@ -123,7 +195,7 @@ gogoanime.fetchEpisodeSources("one-piece-episode-1022").then(data => { console.log(data); } ``` -returns a promise which resolves into an array of episode sources. (*[`Promise<{ headers: { [k: string]: string }; sources: IVideo[] }>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L59-L74)*) +returns a promise which resolves into an array of episode sources. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L210-L214)*)\ output: ```js { @@ -156,7 +228,7 @@ gogoanime.fetchEpisodeServers("one-piece-episode-1022").then(data => { console.log(data); } ``` -returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L54-L57)*) +returns a promise which resolves into an array of episode servers. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L54-L57)*)\ output: ```js [ diff --git a/docs/providers/mangadex.md b/docs/providers/mangadex.md index e066e046d..9cace1275 100644 --- a/docs/providers/mangadex.md +++ b/docs/providers/mangadex.md @@ -26,7 +26,7 @@ mangadex.search("Tomodachi Game").then(data => { console.log(data); } ``` -returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*) +returns a promise which resolves into an array of manga. (*[`Promise>`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L97-L106)*)\ output: ```js { @@ -41,7 +41,7 @@ output: {...}, ... ], - descitption: "Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships of not having enough funds. He works hard to save up in ...', + descitption: "Katagiri Yuichi believes that friends are more important than money, but he also knows the hardships of not having enough funds. He works hard to save up in ...", status: 'ongoing', releaseDate: 2013, contentRating: 'suggestive', @@ -67,7 +67,7 @@ managdex.fetchMangaInfo("b35f67b6-bfb9-4cbd-86f0-621f37e6cb41").then(data => { console.log(data); } ``` -returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*) +returns a promise which resolves into an manga info object (including the chapters). (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L115-L120)*)\ output: ```js { @@ -118,7 +118,7 @@ mangadex.fetchChapterPages("a79255c8-21b5-4a8c-a586-48469fa87020").then(data => console.log(data); } ``` -returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*) +returns an array of pages. (*[`Promise`](https://github.com/consumet/extensions/blob/master/src/models/types.ts#L122-L126)*)\ output: ```js [ diff --git a/docs/providers/readlightnovels.md b/docs/providers/readlightnovels.md index 44b306aa6..2831f9a94 100644 --- a/docs/providers/readlightnovels.md +++ b/docs/providers/readlightnovels.md @@ -24,7 +24,7 @@ readlightnovels.search("Classrrom of the Elite").then(data => { console.log(data); } ``` -returns a promise which resolves into an array of light novels. (* Promise>*) +returns a promise which resolves into an array of light novels. (* Promise>*)\ output: ```js { @@ -55,7 +55,7 @@ readlightnovels.fetchLightNovelInfo("youkoso-jitsuryoku-shijou-shugi-no-kyoushit console.log(data); } ``` -returns a promise which resolves into an light novel info object (including the chapters or volumes). (*Promise\*) +returns a promise which resolves into an light novel info object (including the chapters or volumes). (*Promise\*)\ output: ```js { @@ -97,7 +97,7 @@ readlightnovels.fetchChapterContent("youkoso-jitsuryoku-shijou-shugi-no-kyoushit console.log(data); } ``` -returns a content object. (*Promise\*) +returns a content object. (*Promise\*)\ output: ```js { diff --git a/src/models/types.ts b/src/models/types.ts index f35abaf56..bc93e1972 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -28,7 +28,7 @@ export interface ISearch { export interface IAnimeInfo { id: string; title: string; - url: string; + url?: string; image?: string; releaseDate?: string; genres?: string[]; @@ -69,6 +69,9 @@ export interface IVideo { * make sure to set this to `true` if the video is hls */ isM3U8?: boolean; + /** + * size of the video in **bytes** + */ size?: number; [x: string]: unknown; // other fields } @@ -213,6 +216,9 @@ export interface ISource { sources: IVideo[]; } +/** + * Used **only** for movie/tvshow providers + */ export enum TvType { TVSERIES = 'TV Series', MOVIE = 'Movie', diff --git a/src/providers/anime/animepahe.ts b/src/providers/anime/animepahe.ts new file mode 100644 index 000000000..d33e8721d --- /dev/null +++ b/src/providers/anime/animepahe.ts @@ -0,0 +1,200 @@ +import axios from 'axios'; +import { load } from 'cheerio'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, +} from '../../models'; +import { Kwik } from '../../utils'; + +class AnimePahe extends AnimeParser { + override readonly name = 'AnimePahe'; + protected override baseUrl = 'https://animepahe.com'; + protected override logo = 'hhttps://animepahe.com/pikacon.ico'; + protected override classPath = 'ANIME.AnimePahe'; + + /** + * @param query Search query + */ + override search = async (query: string): Promise> => { + try { + const { data } = await axios.get( + `${this.baseUrl}/api?m=search&q=${encodeURIComponent(query)}` + ); + + const res = { + results: data.data.map((item: any) => ({ + id: item.session, + title: item.title, + image: item.poster, + rating: item.score, + releaseDate: item.year, + type: item.type, + })), + }; + + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * @param id Anime id + * @param episodePage Episode page number (optional) default: -1 to get all episodes. number of episode pages can be found in the anime info object + */ + override fetchAnimeInfo = async (id: string, episodePage: number = -1): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + try { + const res = await axios.get(`${this.baseUrl}/anime/${id}`); + + const $ = load(res.data); + + animeInfo.title = $('div.header-wrapper > header > div > h1 > span').text(); + animeInfo.image = $('header > div > div > div > a > img').attr('data-src'); + animeInfo.cover = `https:${$('body > section > article > div.header-wrapper > div').attr( + 'data-src' + )}`; + animeInfo.description = $('div.col-sm-8.anime-summary > div').text(); + animeInfo.genres = $('div.col-sm-4.anime-info > div > ul > li') + .map((i, el) => $(el).find('a').attr('title')) + .get(); + + switch ($('div.col-sm-4.anime-info > p:nth-child(4) > a').text().trim()) { + case 'Currently Airing': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'Finished Airing': + animeInfo.status = MediaStatus.COMPLETED; + break; + default: + animeInfo.status = MediaStatus.UNKNOWN; + } + animeInfo.type = $('div.col-sm-4.anime-info > p:nth-child(2) > a').text().trim(); + animeInfo.releaseDate = $('div.col-sm-4.anime-info > p:nth-child(5)') + .text() + .split('to')[0] + .replace('Aired:', '') + .trim(); + animeInfo.aired = $('div.col-sm-4.anime-info > p:nth-child(5)') + .text() + .replace('Aired:', '') + .trim() + .replace('\n', ' '); + animeInfo.studios = $('div.col-sm-4.anime-info > p:nth-child(7)') + .text() + .replace('Studio:', '') + .trim() + .split('\n'); + animeInfo.totalEpisodes = parseInt( + $('div.col-sm-4.anime-info > p:nth-child(3)').text().replace('Episodes:', '') + ); + + animeInfo.episodes = []; + if (episodePage < 0) { + const { + data: { last_page, data }, + } = await axios.get(`${this.baseUrl}/api?m=release&id=${id}&sort=episode_asc&page=1`); + + animeInfo.episodePages = last_page; + + animeInfo.episodes.push( + ...data.map( + (item: any) => + ({ + id: item.session, + number: item.episode, + title: item.title, + image: item.snapshot, + duration: item.duration, + } as IAnimeEpisode) + ) + ); + for (let i = 1; i < last_page; i++) { + animeInfo.episodes.push(...(await this.fetchEpisodes(id, i + 1))); + } + } else { + animeInfo.episodes.push(...(await this.fetchEpisodes(id, episodePage))); + } + + return animeInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + try { + const { data } = await axios.get(`${this.baseUrl}/api?m=links&id=${episodeId}`, { + headers: { + Referer: `${this.baseUrl}`, + }, + }); + + const links = data.data.map((item: any) => ({ + quality: Object.keys(item)[0], + iframe: item[Object.keys(item)[0]].kwik, + size: item[Object.keys(item)[0]].filesize, + })); + + const iSource: ISource = { + headers: { + Referer: 'https://kwik.cx/', + }, + sources: [], + }; + for (const link of links) { + const res = await new Kwik().extract(new URL(link.iframe)); + res[0].quality = link.quality; + res[0].size = link.size; + iSource.sources.push(res[0]); + } + + return iSource; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private fetchEpisodes = async (id: string, page: number): Promise => { + const res = await axios.get( + `${this.baseUrl}/api?m=release&id=${id}&sort=episode_asc&page=${page}` + ); + const epData = res.data.data; + + return [ + ...epData.map( + (item: any): IAnimeEpisode => ({ + id: item.session, + number: item.episode, + title: item.title, + image: item.snapshot, + duration: item.duration, + }) + ), + ] as IAnimeEpisode[]; + }; + + /** + * @deprecated + * @attention AnimePahe doesn't support this method + */ + override fetchEpisodeServers = (episodeLink: string): Promise => { + throw new Error('Method not implemented.'); + }; +} + +export default AnimePahe; diff --git a/src/providers/anime/gogoanime.ts b/src/providers/anime/gogoanime.ts index decc792e9..75553b662 100644 --- a/src/providers/anime/gogoanime.ts +++ b/src/providers/anime/gogoanime.ts @@ -18,11 +18,12 @@ import { GogoCDN, StreamSB, USER_AGENT } from '../../utils'; class Gogoanime extends AnimeParser { override readonly name = 'Gogoanime'; protected override baseUrl = 'https://gogoanime.gg'; - protected override logo = 'https://i0.wp.com/cloudfuji.com/wp-content/uploads/2021/12/gogoanime.png?fit=300%2C400&ssl=1'; protected override classPath = 'ANIME.Gogoanime'; + private readonly ajaxUrl = 'https://ajax.gogo-load.com/ajax'; + /** * * @param query search query string @@ -65,30 +66,30 @@ class Gogoanime extends AnimeParser { /** * - * @param animeUrl anime url or id + * @param animeUrl anime id */ - override fetchAnimeInfo = async (animeUrl: string): Promise => { - if (!animeUrl.startsWith(this.baseUrl)) animeUrl = `${this.baseUrl}/category/${animeUrl}`; + override fetchAnimeInfo = async (id: string): Promise => { + if (!id.startsWith(this.baseUrl)) id = `${this.baseUrl}/category/${id}`; const animeInfo: IAnimeInfo = { id: '', title: '', - url: animeUrl, + url: id, genres: [], totalEpisodes: 0, }; try { - const res = await axios.get(animeUrl); + const res = await axios.get(id); const $ = load(res.data); - animeInfo.id = new URL(animeUrl).pathname.split('/')[2]; + animeInfo.id = new URL(id).pathname.split('/')[2]; animeInfo.title = $( 'section.content_left > div.main_body > div:nth-child(2) > div.anime_info_body_bg > h1' ) .text() .trim(); - animeInfo.url = animeUrl; + animeInfo.url = id; animeInfo.image = $('div.anime_info_body_bg > img').attr('src'); animeInfo.releaseDate = $('div.anime_info_body_bg > p:nth-child(7)') .text() @@ -136,7 +137,9 @@ class Gogoanime extends AnimeParser { const alias = $('#alias_anime').attr('value'); const html = await axios.get( - `https://ajax.gogo-load.com/ajax/load-list-episode?ep_start=${ep_start}&ep_end=${ep_end}&id=${movie_id}&default_ep=${0}&alias=${alias}` + `${ + this.ajaxUrl + }/load-list-episode?ep_start=${ep_start}&ep_end=${ep_end}&id=${movie_id}&default_ep=${0}&alias=${alias}` ); const $$ = load(html.data); @@ -246,6 +249,86 @@ class Gogoanime extends AnimeParser { throw new Error('Episode not found.'); } }; + + /** + * @param page page number (optional) + * @param type type of media. (optional) (default `1`) `1`: Japanese with subtitles, `2`: english/dub with no subtitles, `3`: chinese with english subtitles + */ + fetchRecentEpisodes = async ( + page: number = 1, + type: number = 1 + ): Promise> => { + try { + const res = await axios.get( + `${this.ajaxUrl}/page-recent-release.html?page=${page}&type=${type}` + ); + + const $ = load(res.data); + + const recentEpisodes: IAnimeResult[] = []; + + $('div.last_episodes.loaddub > ul > li').each((i, el) => { + recentEpisodes.push({ + id: $(el).find('a').attr('href')?.split('/')[1]?.split('-episode')[0]!, + episodeId: $(el).find('a').attr('href')?.split('/')[1]!, + episodeNumber: parseInt($(el).find('p.episode').text().replace('Episode ', '')), + title: $(el).find('p.name > a').attr('title')!, + image: $(el).find('div > a > img').attr('src'), + url: `${this.baseUrl}${$(el).find('a').attr('href')?.trim()}`, + }); + }); + + const hasNextPage = !$('div.anime_name_pagination.intro > div > ul > li') + .last() + .hasClass('selected'); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: recentEpisodes, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; + + fetchTopAiring = async (page: number = 1): Promise> => { + try { + const res = await axios.get(`${this.ajaxUrl}/page-recent-release-ongoing.html?page=${page}`); + + const $ = load(res.data); + + const topAiring: IAnimeResult[] = []; + + $('div.added_series_body.popular > ul > li').each((i, el) => { + topAiring.push({ + id: $(el).find('a:nth-child(1)').attr('href')?.split('/')[2]!, + title: $(el).find('a:nth-child(1)').attr('title')!, + image: $(el) + .find('a:nth-child(1) > div') + .attr('style') + ?.match('(https?://.*.(?:png|jpg))')![0], + url: `${this.baseUrl}${$(el).find('a:nth-child(1)').attr('href')}`, + genres: $(el) + .find('p.genres > a') + .map((i, el) => $(el).attr('title')) + .get(), + }); + }); + + const hasNextPage = !$('div.anime_name.comedy > div > div > ul > li') + .last() + .hasClass('selected'); + + return { + currentPage: page, + hasNextPage: hasNextPage, + results: topAiring, + }; + } catch (err) { + throw new Error('Something went wrong. Please try again later.'); + } + }; } export default Gogoanime; diff --git a/src/providers/anime/index.ts b/src/providers/anime/index.ts index bc36c97eb..56cc39163 100644 --- a/src/providers/anime/index.ts +++ b/src/providers/anime/index.ts @@ -1,4 +1,5 @@ import Gogoanime from './gogoanime'; import NineAnime from './9anime'; +import AnimePahe from './animepahe'; -export default { Gogoanime, NineAnime }; +export default { Gogoanime, NineAnime, AnimePahe }; diff --git a/src/utils/extractors/index.ts b/src/utils/extractors/index.ts index a24342190..7b66704d3 100644 --- a/src/utils/extractors/index.ts +++ b/src/utils/extractors/index.ts @@ -2,5 +2,6 @@ import GogoCDN from './gogocdn'; import StreamSB from './streamsb'; import VidCloud from './vidcloud'; import MixDrop from './mixdrop'; +import Kwik from './kwik'; -export { GogoCDN, StreamSB, VidCloud, MixDrop }; +export { GogoCDN, StreamSB, VidCloud, MixDrop, Kwik }; diff --git a/src/utils/extractors/kwik.ts b/src/utils/extractors/kwik.ts new file mode 100644 index 000000000..7a3be9d00 --- /dev/null +++ b/src/utils/extractors/kwik.ts @@ -0,0 +1,76 @@ +import axios from 'axios'; +import { load } from 'cheerio'; + +import { VideoExtractor, IVideo } from '../../models'; + +class Kwik extends VideoExtractor { + protected override serverName = 'kwik'; + protected override sources: IVideo[] = []; + + private readonly host = 'https://animepahe.com'; + + override extract = async (videoUrl: URL): Promise => { + try { + const { data } = await axios.get(videoUrl.href, { headers: { Referer: this.host } }); + + const match = load(data) + .html() + .match(/(?<=p}).*(?<=kwik).*}/g); + + if (!match) { + throw new Error('Video not found.'); + } + let arr: string[] = match[0].split('return p}(')[1].split(','); + + const l = arr.slice(0, arr.length - 5).join(''); + arr = arr.slice(arr.length - 5, -1); + arr.unshift(l); + + const [p, a, c, k, e, d] = arr.map((x) => x.split('.sp')[0]); + + const formated = this.format(p, a, c, k, e, {}); + + const source = formated.match(/(?<=source=\\).*(?=\\';)/g)[0].replace(/\'/g, ''); + + this.sources.push({ + url: source, + isM3U8: source.includes('.m3u8'), + }); + + return this.sources; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + private format = (p: any, a: any, c: any, k: any, e: any, d: any) => { + k = k.split('|'); + e = (c: any) => { + return ( + (c < a ? '' : e(parseInt((c / a).toString()))) + + ((c = c % a) > 35 ? String.fromCharCode(c + 29) : c.toString(36)) + ); + }; + if (!''.replace(/^/, String)) { + while (c--) { + d[e(c)] = k[c] || e(c); + } + k = [ + (e: any) => { + return d[e]; + }, + ]; + e = () => { + return '\\w+'; + }; + c = 1; + } + while (c--) { + if (k[c]) { + p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c]); + } + } + return p; + }; +} +export default Kwik; diff --git a/src/utils/extractors/mixdrop.ts b/src/utils/extractors/mixdrop.ts index 275ee7589..85f109499 100644 --- a/src/utils/extractors/mixdrop.ts +++ b/src/utils/extractors/mixdrop.ts @@ -2,7 +2,6 @@ import axios from 'axios'; import { load } from 'cheerio'; import { VideoExtractor, IVideo } from '../../models'; -import { USER_AGENT } from '../'; class MixDrop extends VideoExtractor { protected override serverName = 'MixDrop'; diff --git a/src/utils/index.ts b/src/utils/index.ts index d6f853593..e3b8741d0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { GogoCDN, StreamSB, VidCloud, MixDrop } from './extractors'; +import { GogoCDN, StreamSB, VidCloud, MixDrop, Kwik } from './extractors'; import { USER_AGENT, splitAuthor, @@ -23,4 +23,5 @@ export { countDivs, VidCloud, MixDrop, + Kwik, }; diff --git a/src/utils/providers-list.ts b/src/utils/providers-list.ts index 2e6bfc782..904ff9087 100644 --- a/src/utils/providers-list.ts +++ b/src/utils/providers-list.ts @@ -6,7 +6,7 @@ import { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES } from '../providers' * add new providers here (order does not matter) */ export const PROVIDERS_LIST = { - ANIME: [new ANIME.Gogoanime(), new ANIME.NineAnime()], + ANIME: [new ANIME.Gogoanime(), new ANIME.NineAnime(), new ANIME.AnimePahe()], MANGA: [new MANGA.MangaDex()], BOOKS: [new BOOKS.Libgen()], COMICS: [new COMICS.GetComics()], diff --git a/test/anime/animepahe.test.ts b/test/anime/animepahe.test.ts new file mode 100644 index 000000000..5d2f39a3f --- /dev/null +++ b/test/anime/animepahe.test.ts @@ -0,0 +1,25 @@ +import { ANIME } from '../../src/providers'; + +jest.setTimeout(120000); + +test('returns a filled array of anime list', async () => { + const animepahe = new ANIME.AnimePahe(); + const data = await animepahe.search('Overlord IV'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const animepahe = new ANIME.AnimePahe(); + const data = await animepahe.fetchAnimeInfo('adb84358-8fec-fe80-1dc5-ad6218421dc1'); // Overlord IV id + expect(data).not.toBeNull(); + expect(data.description).not.toBeNull(); + expect(data.episodes).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const animepahe = new ANIME.AnimePahe(); + const data = await animepahe.fetchEpisodeSources( + 'c673b4d6cedf5e4cd1900d30d61ee2130e23a74e58f4401a85f21a4e95c94f73' + ); // Overlord IV episode 1 id + expect(data.sources).not.toEqual([]); +}); diff --git a/test/anime/gogoanime.test.ts b/test/anime/gogoanime.test.ts index c74b5f9c2..3cd9f1375 100644 --- a/test/anime/gogoanime.test.ts +++ b/test/anime/gogoanime.test.ts @@ -5,7 +5,7 @@ jest.setTimeout(120000); test('returns a filled array of anime list', async () => { const gogoanime = new ANIME.Gogoanime(); const data = await gogoanime.search('spy x family'); - expect(data).not.toEqual([]); + expect(data.results).not.toEqual([]); }); test('returns a filled object of anime data', async () => {