From 4e86e0d13253dda00100fdf1dff577ced711a127 Mon Sep 17 00:00:00 2001 From: futaba Date: Wed, 13 Jul 2022 17:26:21 -0400 Subject: [PATCH 1/4] feat: new meta provider --- src/index.ts | 4 +- src/models/anime-parser.ts | 12 +- src/models/base-parser.ts | 2 +- src/models/lightnovel-parser.ts | 2 +- src/models/movie-parser.ts | 2 +- src/models/types.ts | 25 +-- src/providers/anime/9anime.ts | 6 +- src/providers/anime/animepahe.ts | 5 +- src/providers/anime/gogoanime.ts | 1 + src/providers/index.ts | 3 +- src/providers/meta/anilist.ts | 269 +++++++++++++++++++++++++++++++ src/providers/meta/index.ts | 3 + src/utils/index.ts | 6 + src/utils/providers-list.ts | 3 +- src/utils/utils.ts | 7 + 15 files changed, 321 insertions(+), 29 deletions(-) create mode 100644 src/providers/meta/anilist.ts create mode 100644 src/providers/meta/index.ts diff --git a/src/index.ts b/src/index.ts index 817fe7131..f6557b97c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -import { ANIME, BOOKS, COMICS, LIGHT_NOVELS, MANGA, MOVIES } from './providers'; +import { ANIME, BOOKS, COMICS, LIGHT_NOVELS, MANGA, MOVIES, META } from './providers'; import { PROVIDERS_LIST } from './utils/providers-list'; -export { ANIME, BOOKS, COMICS, MANGA, LIGHT_NOVELS, MOVIES }; +export { ANIME, BOOKS, COMICS, MANGA, LIGHT_NOVELS, MOVIES, META }; export { PROVIDERS_LIST }; diff --git a/src/models/anime-parser.ts b/src/models/anime-parser.ts index 58dfb2c7b..d7be3378e 100644 --- a/src/models/anime-parser.ts +++ b/src/models/anime-parser.ts @@ -1,26 +1,26 @@ -import { BaseParser } from '.'; +import { BaseParser, IAnimeInfo, ISource, IEpisodeServer } from '.'; abstract class AnimeParser extends BaseParser { /** - * takes anime link or id + * takes anime id * - * returns anime info + * returns anime info (including episodes) */ - protected abstract fetchAnimeInfo(animeUrl: string): Promise; + abstract fetchAnimeInfo(animeid: string, ...args: any): Promise; /** * takes episode id * * returns episode sources (video links) */ - protected abstract fetchEpisodeSources(episodeId: string, ...args: any): Promise; + abstract fetchEpisodeSources(episodeId: string, ...args: any): Promise; /** * takes episode link * * returns episode servers (video links) available */ - protected abstract fetchEpisodeServers(episodeLink: string): Promise; + abstract fetchEpisodeServers(episodeLink: string): Promise; } export default AnimeParser; diff --git a/src/models/base-parser.ts b/src/models/base-parser.ts index 646310c53..898086b98 100644 --- a/src/models/base-parser.ts +++ b/src/models/base-parser.ts @@ -6,7 +6,7 @@ abstract class BaseParser extends BaseProvider { * * returns a promise resolving to a data object */ - protected abstract search(query: string, ...args: any[]): Promise; + abstract search(query: string, ...args: any[]): Promise; } export default BaseParser; diff --git a/src/models/lightnovel-parser.ts b/src/models/lightnovel-parser.ts index 6dfb4a0ac..19c0678fd 100644 --- a/src/models/lightnovel-parser.ts +++ b/src/models/lightnovel-parser.ts @@ -4,7 +4,7 @@ abstract class LightNovelParser extends BaseParser { /** * takes light novel link or id * - * returns lightNovel info + * returns lightNovel info (including chapters) */ protected abstract fetchLightNovelInfo(lightNovelUrl: string, ...args: any): Promise; diff --git a/src/models/movie-parser.ts b/src/models/movie-parser.ts index ad6c140fd..9834bda3e 100644 --- a/src/models/movie-parser.ts +++ b/src/models/movie-parser.ts @@ -9,7 +9,7 @@ abstract class MovieParser extends BaseParser { /** * takes media link or id * - * returns media info + * returns media info (including episodes) */ protected abstract fetchMediaInfo(mediaUrl: string): Promise; diff --git a/src/models/types.ts b/src/models/types.ts index bc93e1972..393ae923e 100644 --- a/src/models/types.ts +++ b/src/models/types.ts @@ -10,10 +10,17 @@ export interface IProviderStats { isWorking: boolean; } +export interface ITitle { + romaji?: string; + english?: string; + native?: string; + userPreferred?: string; +} + export interface IAnimeResult { id: string; - title: string; - url: string; + title: string | ITitle; + url?: string; image?: string; releaseDate?: string; [x: string]: unknown; // other fields @@ -25,12 +32,7 @@ export interface ISearch { results: T[]; } -export interface IAnimeInfo { - id: string; - title: string; - url?: string; - image?: string; - releaseDate?: string; +export interface IAnimeInfo extends IAnimeResult { genres?: string[]; description?: string; type?: string; @@ -38,7 +40,6 @@ export interface IAnimeInfo { totalEpisodes?: number; subOrDub?: SubOrSub; episodes?: IAnimeEpisode[]; - [x: string]: unknown; // other fields } export interface IAnimeEpisode { @@ -100,7 +101,7 @@ export enum SubOrSub { export interface IMangaResult { id: string; - title: string | [lang: string][]; + title: string | [lang: string][] | ITitle; altTitles?: string | [lang: string][]; image?: string; description?: string | [lang: string][] | { [lang: string]: string }; @@ -131,7 +132,7 @@ export interface IMangaChapterPage { export interface ILightNovelResult { id: string; - title: string; + title: string | ITitle; url: string; image?: string; [x: string]: unknown; // other fields @@ -238,7 +239,7 @@ export interface IMovieEpisode { export interface IMovieResult { id: string; - title: string; + title: string | ITitle; url: string; image?: string; releaseDate?: string; diff --git a/src/providers/anime/9anime.ts b/src/providers/anime/9anime.ts index d09490923..41a55c79a 100644 --- a/src/providers/anime/9anime.ts +++ b/src/providers/anime/9anime.ts @@ -10,6 +10,8 @@ import { MediaStatus, SubOrSub, IAnimeResult, + IEpisodeServer, + ISource, } from '../../models'; /** @@ -186,11 +188,11 @@ class NineAnime extends AnimeParser { } } - override async fetchEpisodeSources(episodeLink: string): Promise { + override async fetchEpisodeSources(episodeLink: string): Promise { throw new Error('Method not implemented.'); } - override async fetchEpisodeServers(episodeLink: string): Promise { + override async fetchEpisodeServers(episodeLink: string): Promise { throw new Error('Method not implemented.'); } diff --git a/src/providers/anime/animepahe.ts b/src/providers/anime/animepahe.ts index d33e8721d..c052a2d2d 100644 --- a/src/providers/anime/animepahe.ts +++ b/src/providers/anime/animepahe.ts @@ -9,13 +9,14 @@ import { IAnimeResult, ISource, IAnimeEpisode, + IEpisodeServer, } 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 logo = 'https://animepahe.com/pikacon.ico'; protected override classPath = 'ANIME.AnimePahe'; /** @@ -192,7 +193,7 @@ class AnimePahe extends AnimeParser { * @deprecated * @attention AnimePahe doesn't support this method */ - override fetchEpisodeServers = (episodeLink: string): Promise => { + override fetchEpisodeServers = (episodeLink: string): Promise => { throw new Error('Method not implemented.'); }; } diff --git a/src/providers/anime/gogoanime.ts b/src/providers/anime/gogoanime.ts index 75553b662..d5f7f983a 100644 --- a/src/providers/anime/gogoanime.ts +++ b/src/providers/anime/gogoanime.ts @@ -151,6 +151,7 @@ class Gogoanime extends AnimeParser { url: `${this.baseUrl}/${$(el).find(`a`).attr('href')?.trim()}`, }); }); + animeInfo.episodes = animeInfo.episodes.reverse(); animeInfo.totalEpisodes = parseInt(ep_end ?? '0'); diff --git a/src/providers/index.ts b/src/providers/index.ts index 0ac895dd1..68b5d36b5 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -4,5 +4,6 @@ import LIGHT_NOVELS from './light-novels'; import BOOKS from './books'; import COMICS from './comics'; import MOVIES from './movies'; +import META from './meta'; -export { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES }; +export { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES, META }; diff --git a/src/providers/meta/anilist.ts b/src/providers/meta/anilist.ts new file mode 100644 index 000000000..eb1d87549 --- /dev/null +++ b/src/providers/meta/anilist.ts @@ -0,0 +1,269 @@ +import axios from 'axios'; + +import { + AnimeParser, + ISearch, + IAnimeInfo, + MediaStatus, + IAnimeResult, + ISource, + IAnimeEpisode, + SubOrSub, + IEpisodeServer, +} from '../../models'; +import { anilistSearchQuery, anilistMediaDetailQuery, kitsuSearchQuery } from '../../utils'; +import Gogoanime from '../../providers/anime/gogoanime'; + +class Anilist extends AnimeParser { + override readonly name = 'AnimePahe'; + protected override baseUrl = 'https://anilist.co/'; + protected override logo = 'https://anilist.co/img/icons/icon.svg'; + protected override classPath = 'META.Anilist'; + + private readonly anilistGraphqlUrl = 'https://graphql.anilist.co'; + private readonly kitsuGraphqlUrl = 'https://kitsu.io/api/graphql'; + private provider: AnimeParser; + + /** + * This class maps anilist to kitsu, and anime provider + * @param provider anime provider (optional) default: Gogoanime + */ + constructor(provider?: AnimeParser) { + super(); + this.provider = provider || new Gogoanime(); + } + + /** + * @param query Search query + * @param page Page number (optional) + * @param perPage Number of results per page (optional) (default: 15) (max: 50) + */ + override search = async ( + query: string, + page: number = 1, + perPage: number = 15 + ): Promise> => { + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistSearchQuery(query, page, perPage), + }; + + try { + const { data } = await axios.post(this.anilistGraphqlUrl, options); + + const res: ISearch = { + currentPage: data.data.Page.pageInfo.currentPage, + hasNextPage: data.data.Page.pageInfo.hasNextPage, + results: data.data.Page.media.map((item: any) => ({ + id: item.id.toString(), + title: + { + romaji: item.title.romaji, + english: item.title.english, + native: item.title.native, + userPreferred: item.title.userPreferred, + } || item.title.romaji, + image: item.coverImage.large, + rating: item.averageScore, + releasedDate: item.seasonYear, + })), + }; + + return res; + } catch (err) { + throw new Error((err as Error).message); + } + }; + /** + * + * @param id Anime id + * @param dub to get dubbed episodes (optional) set to `true` to get dubbed episodes. **ONLY WORKS FOR GOGOANIME** + */ + override fetchAnimeInfo = async (id: string, dub: boolean = false): Promise => { + const animeInfo: IAnimeInfo = { + id: id, + title: '', + }; + + const options = { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + query: anilistMediaDetailQuery(id), + }; + + try { + const { data } = await axios.post(this.anilistGraphqlUrl, options); + animeInfo.malId = data.data.Media.idMal.toString(); + animeInfo.title = { + romaji: data.data.Media.title.romaji, + english: data.data.Media.title.english, + native: data.data.Media.title.native, + userPreferred: data.data.Media.title.userPreferred, + }; + + animeInfo.image = data.data.Media.coverImage.large; + animeInfo.description = data.data.Media.description; + animeInfo.releaseDate = data.data.Media.startDate.year; + animeInfo.rating = data.data.Media.averageScore; + animeInfo.duration = data.data.Media.duration; + animeInfo.subOrDub = dub ? SubOrSub.DUB : SubOrSub.SUB; + + const possibleAnimeEpisodes = await this.findAnime( + { english: animeInfo.title?.english!, romaji: animeInfo.title?.romaji! }, + data.data.Media.season!, + data.data.Media.startDate.year + ); + + if (possibleAnimeEpisodes) { + animeInfo.episodes = possibleAnimeEpisodes; + if (this.provider.name === 'Gogoanime' && dub) { + animeInfo.episodes = animeInfo.episodes.map((episode: IAnimeEpisode) => { + const [episodeSlug, episodeNumber] = episode.id.split('-episode-')[0]; + episode.id = `${episodeSlug.replace(/-movie$/, '')}-dub-episode-${episodeNumber}`; + return episode; + }); + } + } + + animeInfo.episodes = animeInfo.episodes?.map((episode: IAnimeEpisode) => { + if (!episode.image) { + episode.image = animeInfo.image; + } + return episode; + }); + + return animeInfo; + } catch (err) { + throw new Error((err as Error).message); + } + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeSources = async (episodeId: string): Promise => { + return this.provider.fetchEpisodeSources(episodeId); + }; + + /** + * + * @param episodeId Episode id + */ + override fetchEpisodeServers = async (episodeId: string): Promise => { + return this.provider.fetchEpisodeServers(episodeId); + }; + + private findAnime = async ( + title: { romaji: string; english: string }, + season: string, + startDate: number + ): Promise => { + title.english = title.english || title.romaji; + title.romaji = title.romaji || title.english; + + title.english = title.english.toLocaleLowerCase(); + title.romaji = title.romaji.toLocaleLowerCase(); + + if (title.english === title.romaji) { + return await this.findAnimeSlug(title.english, season, startDate); + } + + const romajiAnime = this.findAnimeSlug(title.romaji, season, startDate); + const englishAnime = this.findAnimeSlug(title.english, season, startDate); + + const episodes = await Promise.all([englishAnime, romajiAnime]).then((r) => + r[0].length > 0 ? r[0] : r[1] + ); + + return episodes; + }; + + private findAnimeSlug = async ( + title: string, + season: string, + startDate: number + ): Promise => { + const slug = title.replace(/[^0-9a-zA-Z]+/g, ' '); + + const findAnime = (await this.provider.search(slug)) as ISearch; + + if (findAnime.results.length === 0) return []; + + const possibleAnime = (await this.provider.fetchAnimeInfo( + findAnime.results[0].id + )) as IAnimeInfo; + + const possibleProviderEpisodes = possibleAnime.episodes; + + const options = { + headers: { 'Content-Type': 'application/json' }, + query: kitsuSearchQuery(slug), + }; + + const kitsuEpisodes = await axios.post(this.kitsuGraphqlUrl, options); + const episodesList = new Map(); + + if (kitsuEpisodes?.data.data) { + const { nodes } = kitsuEpisodes.data.data.searchAnimeByTitle; + + if (nodes) { + nodes.forEach((node: any) => { + if ( + node.season === season && + node.startDate.trim().split('-')[0] === startDate.toString() + ) { + const episodes = node.episodes.nodes; + + episodes.forEach((episode: any) => { + if (episode) { + const i = episode.number.toString().replace('"', ''); + let name = null; + let description = null; + let thumbnail = null; + if (episode.titles?.canonical) + name = episode.titles.canonical.toString().replace('"', ''); + if (episode.description?.en) + description = episode.description.en + .toString() + .replace('"', '') + .replace('\\n', '\n'); + if (episode.thumbnail) + thumbnail = episode.thumbnail.original.url.toString().replace('"', ''); + episodesList.set(i, { + episodeNum: episode.number.toString().replace('"', ''), + title: name, + description, + thumbnail, + }); + } + }); + } + }); + } + } + const newEpisodeList: IAnimeEpisode[] = []; + + if (episodesList.size !== 0 && possibleProviderEpisodes?.length !== 0) { + possibleProviderEpisodes?.reverse(); + possibleProviderEpisodes?.forEach((ep, i) => { + const j = (i + 1).toString(); + newEpisodeList.push({ + id: ep.id as string, + title: episodesList.get(j)?.title ?? null, + image: episodesList.get(j)?.thumbnail ?? null, + number: ep.number as number, + description: episodesList.get(j)?.description, + }); + }); + } + return newEpisodeList; + }; +} + +export default Anilist; diff --git a/src/providers/meta/index.ts b/src/providers/meta/index.ts new file mode 100644 index 000000000..4c9804f36 --- /dev/null +++ b/src/providers/meta/index.ts @@ -0,0 +1,3 @@ +import Anilist from './anilist'; + +export default { Anilist }; diff --git a/src/utils/index.ts b/src/utils/index.ts index e3b8741d0..9a967f59b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,6 +6,9 @@ import { formatTitle, genElement, capitalizeFirstLetter, + anilistSearchQuery, + anilistMediaDetailQuery, + kitsuSearchQuery, } from './utils'; import { parsePostInfo } from './getComics'; import { countDivs } from './zLibrary'; @@ -24,4 +27,7 @@ export { VidCloud, MixDrop, Kwik, + anilistSearchQuery, + anilistMediaDetailQuery, + kitsuSearchQuery, }; diff --git a/src/utils/providers-list.ts b/src/utils/providers-list.ts index 904ff9087..9c5486c01 100644 --- a/src/utils/providers-list.ts +++ b/src/utils/providers-list.ts @@ -1,4 +1,4 @@ -import { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES } from '../providers'; +import { ANIME, MANGA, BOOKS, COMICS, LIGHT_NOVELS, MOVIES, META } from '../providers'; /** * List of providers @@ -12,5 +12,6 @@ export const PROVIDERS_LIST = { COMICS: [new COMICS.GetComics()], LIGHT_NOVELS: [new LIGHT_NOVELS.ReadLightNovels()], MOVIES: [new MOVIES.FlixHQ()], + META: [new META.Anilist()], OTHERS: [], }; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1a8d22015..539363016 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -63,3 +63,10 @@ export const genElement = (s: string, e: string) => { }; export const capitalizeFirstLetter = (s: string) => s.charAt(0).toUpperCase() + s.slice(1); + +export const anilistSearchQuery = (query: string, page: number, perPage: number) => + `query ($page: Int = ${page}, $id: Int, $type: MediaType = ANIME, $search: String = "${query}", $isAdult: Boolean = false, $size: Int = ${perPage}) { Page(page: $page, perPage: $size) { pageInfo { total perPage currentPage lastPage hasNextPage } media(id: $id, type: $type, search: $search, isAdult: $isAdult) { id idMal status(version: 2) title { userPreferred romaji english native } bannerImage coverImage{ extraLarge large medium color } episodes meanScore season seasonYear averageScore nextAiringEpisode { airingAt timeUntilAiring episode } } } }`; +export const anilistMediaDetailQuery = (id: string) => + `query ($id: Int = ${id}) { Media(id: $id) { id idMal title { userPreferred english native romaji } coverImage { extraLarge large color } startDate { year month day } endDate { year month day } bannerImage season seasonYear description type format status(version: 2) episodes duration chapters volumes genres source averageScore popularity meanScore nextAiringEpisode { airingAt timeUntilAiring episode } characters(sort: ROLE) { edges { role node { id name { first middle last full native userPreferred } image { large medium } } } } recommendations { edges { node { id mediaRecommendation { id idMal title { romaji english native userPreferred } status episodes coverImage { extraLarge large medium color } bannerImage meanScore nextAiringEpisode { episode timeUntilAiring airingAt } } } } } relations { edges { id node { id idMal status coverImage { extraLarge large medium color } bannerImage title { romaji english native userPreferred } episodes nextAiringEpisode { airingAt timeUntilAiring episode } meanScore } } } streamingEpisodes { title thumbnail url } studios { edges { isMain node { id name } } } } }`; +export const kitsuSearchQuery = (query: string) => + `query{searchAnimeByTitle(first:5, title:"${query}"){ nodes {id season startDate titles { localized } episodes(first: 2000){ nodes { number titles { canonical } description thumbnail { original { url } } } } } } }`; From d2d7bc0d8a523761bc4163c61e1204c3436f3fa2 Mon Sep 17 00:00:00 2001 From: futaba Date: Wed, 13 Jul 2022 17:35:52 -0400 Subject: [PATCH 2/4] test(meta(anilist)): added new tests --- src/providers/meta/anilist.ts | 19 +++++++++++++++++++ test/meta/anilist.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 test/meta/anilist.test.ts diff --git a/src/providers/meta/anilist.ts b/src/providers/meta/anilist.ts index eb1d87549..246769916 100644 --- a/src/providers/meta/anilist.ts +++ b/src/providers/meta/anilist.ts @@ -108,9 +108,28 @@ class Anilist extends AnimeParser { animeInfo.image = data.data.Media.coverImage.large; animeInfo.description = data.data.Media.description; + switch (data.data.Media.status) { + case 'RELEASING': + animeInfo.status = MediaStatus.ONGOING; + break; + case 'FINISHED': + animeInfo.status = MediaStatus.COMPLETED; + break; + case 'NOT_YET_RELEASED': + animeInfo.status = MediaStatus.NOT_YET_AIRED; + break; + case 'CANCELLED': + animeInfo.status = MediaStatus.CANCELLED; + break; + case 'HIATUS': + animeInfo.status = MediaStatus.HIATUS; + default: + animeInfo.status = MediaStatus.UNKNOWN; + } animeInfo.releaseDate = data.data.Media.startDate.year; animeInfo.rating = data.data.Media.averageScore; animeInfo.duration = data.data.Media.duration; + animeInfo.genres = data.data.Media.genres; animeInfo.subOrDub = dub ? SubOrSub.DUB : SubOrSub.SUB; const possibleAnimeEpisodes = await this.findAnime( diff --git a/test/meta/anilist.test.ts b/test/meta/anilist.test.ts new file mode 100644 index 000000000..10ff2b203 --- /dev/null +++ b/test/meta/anilist.test.ts @@ -0,0 +1,29 @@ +import { META } from '../../src/providers'; + +jest.setTimeout(120000); + +test('returns a filled array of anime list', async () => { + const anilist = new META.Anilist(); + const data = await anilist.search('spy x family'); + expect(data.results).not.toEqual([]); +}); + +test('returns a filled object of anime data', async () => { + const anilist = new META.Anilist(); + const data = await anilist.fetchAnimeInfo('140960'); + expect(data).not.toBeNull(); + expect(data.episodes).not.toEqual([]); + expect(data.description).not.toBeNull(); +}); + +test('returns a filled array of servers', async () => { + const anilist = new META.Anilist(); + const data = await anilist.fetchEpisodeServers('spy-x-family-episode-9'); + expect(data).not.toEqual([]); +}); + +test('returns a filled object of episode sources', async () => { + const anilist = new META.Anilist(); + const data = await anilist.fetchEpisodeSources('spy-x-family-episode-9'); + expect(data.sources).not.toEqual([]); +}); From 28fb0c5a548d5cce120f7ea35680d1fa04ca89f4 Mon Sep 17 00:00:00 2001 From: futaba Date: Wed, 13 Jul 2022 18:17:45 -0400 Subject: [PATCH 3/4] feat(anilist): updated docs --- README.md | 1 + docs/README.md | 1 + docs/guides/meta.md | 20 +++++ docs/providers/anilist.md | 159 ++++++++++++++++++++++++++++++++++ src/providers/meta/anilist.ts | 18 ++-- 5 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 docs/guides/meta.md create mode 100644 docs/providers/anilist.md diff --git a/README.md b/README.md index acb22a6c8..f0f10fe51 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Do you want to know more? Head to the [`Getting Started`](https://github.com/con - [`Movies`](./docs/guides/movies.md) - [`Light Novels`](./docs/guides/light-novels.md) - [`Comics`](./docs/guides/comics.md) +- [`Meta`](./docs/guides/meta.md) ## Ecosystem - [Rest-API Reference](https://docs.consumet.org/) - public rest api documentation diff --git a/docs/README.md b/docs/README.md index 67110a3ae..1fb718d80 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,5 +8,6 @@ - [Movie](./guides/movies.md): How to use movie providers. - [Light Novels](./guides/light-novels.md): How to use light novel providers. - [Comics](./guides/comics.md): How to use comic providers. +- [Meta](./guides/meta.md): How to use meta providers. - [Benchmarks](https://github.com/consumet/providers-status#readme): Real-time benchmarking of providers. - [Contributing](./guides/contributing.md): Details about how to contribute to Consumet Extensions. diff --git a/docs/guides/meta.md b/docs/guides/meta.md new file mode 100644 index 000000000..8ed300f53 --- /dev/null +++ b/docs/guides/meta.md @@ -0,0 +1,20 @@ +

Consumet Extensions

+ +

META

+ +By using `META` category you can interact with the custom providers. And get access to the meta providers methods. + +```ts +// ESM +import { META } from '@consumet/extensions'; + +// is the name of the provider you want to use. list of the proivders is below. +const metaProvider = META.(); +``` + +## Meta Providers List +This list is in alphabetical order. (except the sub bullet points) + +- [Anilist](../providers/anilist.md) + +

(back to table of contents)

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

Anilist

+This is a custom provider that maps an anime provider (like gogoanime) to anilist and kitsu. + +`Anilist` class takes a [`AnimeParser`](https://github.com/consumet/extensions/blob/master/src/models/anime-parser.ts) object as a parameter **(optional)**. This object is used to parse the anime episodes from the provider, then mapped to anilist and kitsu. + +```ts +const anilist = new META.Anilist(); +``` + +

Methods

+ +- [search](#search) +- [fetchAnimeInfo](#fetchanimeinfo) +- [fetchEpisodeSources](#fetchepisodesources) + +### search + +

Parameters

+ +| Parameter | Type | Description | +| -------------------- | -------- | ----------------------------------------------------------------------------------- | +| query | `string` | query to search for. (*In this case, We're searching for `Classroom of the elite`*) | +| page (optional) | `number` | page number to search for. | +| perPage (optional) | `number` | number of results per page. **Default: 15** | + +```ts +anilist.search("Classroom of the elite").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 +{ + currentPage: 1, + hasNextPage: false, + results: [ + { + id: '98659', + title: { + romaji: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e', + english: 'Classroom of the Elite', + native: 'ようこそ実力至上主義の教室へ', + userPreferred: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b98659-sH5z5RfMuyMr.png', + rating: 77, + releasedDate: 2017 + }, + { + id: '145545', + title: { + romaji: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e 2nd Season', + english: 'Classroom of the Elite Season 2', + native: 'ようこそ実力至上主義の教室へ 2nd Season', + userPreferred: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e 2nd Season' + }, + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/bx145545-DGl3LVvFlnHi.png', + rating: 79, + releasedDate: 2022 + } + {...} + ... + ] +} +``` + +### 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*) | +| dub (optional) | `boolean` | if true, will fetch dubbed anime. **Default: false** | + + +```ts +anilist.fetchAnimeInfo("98659").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: '98659', + title: { + romaji: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e', + english: 'Classroom of the Elite', + native: 'ようこそ実力至上主義の教室へ', + userPreferred: 'Youkoso Jitsuryoku Shijou Shugi no Kyoushitsu e' + }, + malId: '35507', + image: 'https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/b98659-sH5z5RfMuyMr.png', + description: 'Koudo Ikusei Senior High School is a leading school with state-of-the-art facilities. The students there have the freedom to wear any hairstyle ...', + status: 'Completed', + releaseDate: 2017, + rating: 77, + duration: 24, + genres: [ 'Drama', 'Psychological' ], + subOrDub: 'sub', + episodes: [ + { + id: 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-12', + title: 'What is evil? Whatever springs from weakness.', + image: 'https://media.kitsu.io/episodes/thumbnails/228542/original.jpg', + number: 12, + description: "Melancholy, unmotivated Ayanokoji Kiyotaka attends his first day at Tokyo Metropoiltan Advanced Nuturing High School, ..." + }, + {...} + ... + ] +} +``` + +### 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 classroom of the elite. + + +```ts +anilist.fetchEpisodeSources("youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-12").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://goload.pro/streaming.php?id=MTAxMTU3&title=Youkoso+Jitsuryoku+Shijou+Shugi+no+Kyoushitsu+e+%28TV%29+Episode+12&typesub=SUB' + }, + sources: [ + { + url: 'https://manifest.prod.boltdns.net/manifest/v1/hls/v4/clear/6310475588001/d34ba94f-c1db-4b05-a0b2-34d5a40134b2/6s/master.m3u8?fastly_token=NjJjZjkxZGFfODlmNWQyMWU1ZDM1NzhlNWM1MGMyMTBkNjczMjY4YjQ5ZGMyMzEzMWI2YjgyZjVhNWRhMDU4YmI0NjFjMTY4Zg%3D%3D', + isM3U8: true + }, + { + url: 'https://www13.gogocdn.stream/hls/ba0b5d73fb1737d2e8007c65f347dae8/ep.12.1649784300.m3u8', + isM3U8: true + } + ] +} +``` + +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 meta providers list)

\ No newline at end of file diff --git a/src/providers/meta/anilist.ts b/src/providers/meta/anilist.ts index 246769916..e397159ed 100644 --- a/src/providers/meta/anilist.ts +++ b/src/providers/meta/anilist.ts @@ -241,21 +241,21 @@ class Anilist extends AnimeParser { episodes.forEach((episode: any) => { if (episode) { - const i = episode.number.toString().replace('"', ''); + const i = episode.number.toString().replace(/"/g, ''); let name = null; let description = null; let thumbnail = null; if (episode.titles?.canonical) - name = episode.titles.canonical.toString().replace('"', ''); + name = episode.titles.canonical.toString().replace(/"/g, ''); if (episode.description?.en) description = episode.description.en .toString() - .replace('"', '') + .replace(/"/g, '') .replace('\\n', '\n'); if (episode.thumbnail) - thumbnail = episode.thumbnail.original.url.toString().replace('"', ''); + thumbnail = episode.thumbnail.original.url.toString().replace(/"/g, ''); episodesList.set(i, { - episodeNum: episode.number.toString().replace('"', ''), + episodeNum: episode.number.toString().replace(/"/g, ''), title: name, description, thumbnail, @@ -285,4 +285,12 @@ class Anilist extends AnimeParser { }; } +(async () => { + const provider = new Anilist(); + const anime = await provider.fetchEpisodeSources( + 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-12' + ); + console.log(anime); +})(); + export default Anilist; From 7ec24b5966989c4670efee3ba0cb8ffd237f1000 Mon Sep 17 00:00:00 2001 From: futaba Date: Wed, 13 Jul 2022 18:39:32 -0400 Subject: [PATCH 4/4] feat: cleanup --- package.json | 5 +++-- src/providers/anime/animepahe.ts | 2 ++ src/providers/meta/anilist.ts | 8 -------- test/anime/animepahe.test.ts | 9 +++++---- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 1c677edb0..e243b212a 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "test:comics": "jest ./test/comics", "test:movies": "jest ./test/movies", "test:manga": "jest ./test/manga", - "test:lightnovels": "jest ./test/light-novels" + "test:lightnovels": "jest ./test/light-novels", + "test:meta": "jest ./test/meta" }, "files": [ "dist" @@ -59,4 +60,4 @@ "test": "test", "lib": "src" } -} +} \ No newline at end of file diff --git a/src/providers/anime/animepahe.ts b/src/providers/anime/animepahe.ts index c052a2d2d..3222d54e7 100644 --- a/src/providers/anime/animepahe.ts +++ b/src/providers/anime/animepahe.ts @@ -120,6 +120,7 @@ class AnimePahe extends AnimeParser { } as IAnimeEpisode) ) ); + for (let i = 1; i < last_page; i++) { animeInfo.episodes.push(...(await this.fetchEpisodes(id, i + 1))); } @@ -174,6 +175,7 @@ class AnimePahe extends AnimeParser { const res = await axios.get( `${this.baseUrl}/api?m=release&id=${id}&sort=episode_asc&page=${page}` ); + console.log(`${this.baseUrl}/api?m=release&id=${id}&sort=episode_asc&page=${page}`); const epData = res.data.data; return [ diff --git a/src/providers/meta/anilist.ts b/src/providers/meta/anilist.ts index e397159ed..1f409748e 100644 --- a/src/providers/meta/anilist.ts +++ b/src/providers/meta/anilist.ts @@ -285,12 +285,4 @@ class Anilist extends AnimeParser { }; } -(async () => { - const provider = new Anilist(); - const anime = await provider.fetchEpisodeSources( - 'youkoso-jitsuryoku-shijou-shugi-no-kyoushitsu-e-tv-episode-12' - ); - console.log(anime); -})(); - export default Anilist; diff --git a/test/anime/animepahe.test.ts b/test/anime/animepahe.test.ts index 5d2f39a3f..a3c7bc033 100644 --- a/test/anime/animepahe.test.ts +++ b/test/anime/animepahe.test.ts @@ -10,7 +10,8 @@ test('returns a filled array of anime list', async () => { 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 + const res = await animepahe.search('Overlord IV'); + const data = await animepahe.fetchAnimeInfo(res.results[0].id); // Overlord IV id expect(data).not.toBeNull(); expect(data.description).not.toBeNull(); expect(data.episodes).not.toEqual([]); @@ -18,8 +19,8 @@ test('returns a filled object of anime data', async () => { 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 + const res = await animepahe.search('Overlord IV'); + const info = await animepahe.fetchAnimeInfo(res.results[0].id); + const data = await animepahe.fetchEpisodeSources(info.episodes![0].id); // Overlord IV episode 1 id expect(data.sources).not.toEqual([]); });