.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | -------
3 |
4 | An API to effortlessly access information on anime from gogoanime website.
5 |
6 |
7 | ‼️ This API has been discontinued as the gogoanime website has been takendown. ‼️
8 |
9 |
10 | Table of Contents
11 |
12 | - [Installation](#installation)
13 | - [Locally](#locally)
14 | - [Heroku](#heroku)
15 | - [Vercel](#vercel)
16 | - [Render](#render)
17 | - [Documentation](#documentation)
18 | - [Development](#development)
19 | - [Provider Request](#provider-request)
20 |
21 | ## Installation
22 | ### Locally
23 | installation is simple.
24 |
25 | Run the following command to clone the repository, and install the dependencies.
26 |
27 | ```sh
28 | $ git clone https://github.com/avalynndev/animetize-api.git
29 | $ cd animetize-api
30 | $ npm install #or yarn install
31 | ```
32 |
33 | start the server!
34 |
35 | ```sh
36 | $ npm start #or yarn start
37 | ```
38 |
39 | ### Heroku
40 | Host your own instance of Animetize API on Heroku using the button below.
41 |
42 | [](https://heroku.com/deploy?template=https://github.com/avalynndev/animetize-api/tree/main)
43 |
44 | ### Vercel
45 | Host your own instance of Animetize API on Vercel using the button below.
46 |
47 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Favalynndev%2Fanimetize-api)
48 |
49 | ### Render
50 | Host your own instance of Animetize API on Render using the button below.
51 |
52 | [](https://render.com/deploy?repo=https://github.com/avalynndev/animetize-api)
53 |
54 | ## Documentation
55 | Please refer to the [documentation](https://animetize-docs.vercel.app/). If you need any additional help or have any questions, comments, or suggestions, just create a new issue and tag it as "help".
56 |
57 | ## Development
58 | Pull requests and stars are always welcome, for bugs and features create a new [issue](https://github.com/avalynndev/animetize-api/issues).
59 |
60 | ## Provider Request
61 | Make a new [issue](https://github.com/avalynndev/animetize-api/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.
62 |
--------------------------------------------------------------------------------
/extractors/gogocdn.ts:
--------------------------------------------------------------------------------
1 | import { CheerioAPI, load } from 'cheerio';
2 | import CryptoJS from 'crypto-js';
3 |
4 | import { VideoExtractor, IVideo } from '../models';
5 |
6 | class GogoCDN extends VideoExtractor {
7 | protected override serverName = 'goload';
8 | protected override sources: IVideo[] = [];
9 |
10 | private readonly keys = {
11 | key: CryptoJS.enc.Utf8.parse('37911490979715163134003223491201'),
12 | secondKey: CryptoJS.enc.Utf8.parse('54674138327930866480207815084989'),
13 | iv: CryptoJS.enc.Utf8.parse('3134003223491201'),
14 | };
15 |
16 | private referer: string = '';
17 |
18 | override extract = async (videoUrl: URL): Promise => {
19 | this.referer = videoUrl.href;
20 |
21 | const res = await this.client.get(videoUrl.href);
22 | const $ = load(res.data);
23 |
24 | const encyptedParams = await this.generateEncryptedAjaxParams($, videoUrl.searchParams.get('id') ?? '');
25 |
26 | const encryptedData = await this.client.get(
27 | `${videoUrl.protocol}//${videoUrl.hostname}/encrypt-ajax.php?${encyptedParams}`,
28 | {
29 | headers: {
30 | 'X-Requested-With': 'XMLHttpRequest',
31 | },
32 | }
33 | );
34 |
35 | const decryptedData = await this.decryptAjaxData(encryptedData.data.data);
36 | if (!decryptedData.source) throw new Error('No source found. Try a different server.');
37 |
38 | if (decryptedData.source[0].file.includes('.m3u8')) {
39 | const resResult = await this.client.get(decryptedData.source[0].file.toString());
40 | const resolutions = resResult.data.match(/(RESOLUTION=)(.*)(\s*?)(\s*.*)/g);
41 | resolutions?.forEach((res: string) => {
42 | const index = decryptedData.source[0].file.lastIndexOf('/');
43 | const quality = res.split('\n')[0].split('x')[1].split(',')[0];
44 | const url = decryptedData.source[0].file.slice(0, index);
45 | this.sources.push({
46 | url: url + '/' + res.split('\n')[1],
47 | isM3U8: (url + res.split('\n')[1]).includes('.m3u8'),
48 | quality: quality + 'p',
49 | });
50 | });
51 |
52 | decryptedData.source.forEach((source: any) => {
53 | this.sources.push({
54 | url: source.file,
55 | isM3U8: source.file.includes('.m3u8'),
56 | quality: 'default',
57 | });
58 | });
59 | } else
60 | decryptedData.source.forEach((source: any) => {
61 | this.sources.push({
62 | url: source.file,
63 | isM3U8: source.file.includes('.m3u8'),
64 | quality: source.label.split(' ')[0] + 'p',
65 | });
66 | });
67 |
68 | decryptedData.source_bk.forEach((source: any) => {
69 | this.sources.push({
70 | url: source.file,
71 | isM3U8: source.file.includes('.m3u8'),
72 | quality: 'backup',
73 | });
74 | });
75 |
76 | return this.sources;
77 | };
78 |
79 | private addSources = async (source: any) => {
80 | if (source.file.includes('m3u8')) {
81 | const m3u8Urls = await this.client
82 | .get(source.file, {
83 | headers: {
84 | Referer: this.referer,
85 | 'User-Agent':
86 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
87 | },
88 | })
89 | .catch(() => null);
90 |
91 | const videoList = m3u8Urls?.data.split('#EXT-X-I-FRAME-STREAM-INF:');
92 | for (const video of videoList ?? []) {
93 | if (!video.includes('m3u8')) continue;
94 |
95 | const url = video
96 | .split('\n')
97 | .find((line: any) => line.includes('URI='))
98 | .split('URI=')[1]
99 | .replace(/"/g, '');
100 |
101 | const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1];
102 |
103 | this.sources.push({
104 | url: url,
105 | quality: `${quality}p`,
106 | isM3U8: true,
107 | });
108 | }
109 |
110 | return;
111 | }
112 | this.sources.push({
113 | url: source.file,
114 | isM3U8: source.file.includes('.m3u8'),
115 | });
116 | };
117 |
118 | private generateEncryptedAjaxParams = async ($: CheerioAPI, id: string): Promise => {
119 | const encryptedKey = CryptoJS.AES.encrypt(id, this.keys.key, {
120 | iv: this.keys.iv,
121 | });
122 |
123 | const scriptValue = $("script[data-name='episode']").attr('data-value') as string;
124 |
125 | const decryptedToken = CryptoJS.AES.decrypt(scriptValue, this.keys.key, {
126 | iv: this.keys.iv,
127 | }).toString(CryptoJS.enc.Utf8);
128 |
129 | return `id=${encryptedKey}&alias=${id}&${decryptedToken}`;
130 | };
131 |
132 | private decryptAjaxData = async (encryptedData: string): Promise => {
133 | const decryptedData = CryptoJS.enc.Utf8.stringify(
134 | CryptoJS.AES.decrypt(encryptedData, this.keys.secondKey, {
135 | iv: this.keys.iv,
136 | })
137 | );
138 |
139 | return JSON.parse(decryptedData);
140 | };
141 | }
142 |
143 | export default GogoCDN;
144 |
--------------------------------------------------------------------------------
/extractors/index.ts:
--------------------------------------------------------------------------------
1 | import GogoCDN from './gogocdn';
2 | import StreamSB from './streamsb';
3 |
4 | export {
5 | GogoCDN,
6 | StreamSB,
7 | };
8 |
--------------------------------------------------------------------------------
/extractors/streamsb.ts:
--------------------------------------------------------------------------------
1 | import { VideoExtractor, IVideo } from '../models';
2 |
3 | class StreamSB extends VideoExtractor {
4 | protected override serverName = 'streamsb';
5 | protected override sources: IVideo[] = [];
6 |
7 | private readonly host = 'https://streamsss.net/sources50';
8 | // TODO: update host2
9 | private readonly host2 = 'https://watchsb.com/sources50';
10 |
11 | private PAYLOAD = (hex: string) =>
12 | `566d337678566f743674494a7c7c${hex}7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362`;
13 |
14 | override extract = async (videoUrl: URL, isAlt: boolean = false): Promise => {
15 | let headers: any = {
16 | watchsb: 'sbstream',
17 | 'User-Agent':
18 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
19 | Referer: videoUrl.href,
20 | };
21 | let id = videoUrl.href.split('/e/').pop();
22 | if (id?.includes('html')) id = id.split('.html')[0];
23 | const bytes = new TextEncoder().encode(id);
24 |
25 | const res = await this.client
26 | .get(`${isAlt ? this.host2 : this.host}/${this.PAYLOAD(Buffer.from(bytes).toString('hex'))}`, {
27 | headers,
28 | })
29 | .catch(() => null);
30 |
31 | if (!res?.data.stream_data) throw new Error('No source found. Try a different server.');
32 |
33 | headers = {
34 | 'User-Agent':
35 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
36 | Referer: videoUrl.href.split('e/')[0],
37 | };
38 | const m3u8Urls = await this.client.get(res.data.stream_data.file, {
39 | headers,
40 | });
41 |
42 | const videoList = m3u8Urls.data.split('#EXT-X-STREAM-INF:');
43 |
44 | for (const video of videoList ?? []) {
45 | if (!video.includes('m3u8')) continue;
46 |
47 | const url = video.split('\n')[1];
48 | const quality = video.split('RESOLUTION=')[1].split(',')[0].split('x')[1];
49 |
50 | this.sources.push({
51 | url: url,
52 | quality: `${quality}p`,
53 | isM3U8: true,
54 | });
55 | }
56 |
57 | this.sources.push({
58 | quality: 'auto',
59 | url: res.data.stream_data.file,
60 | isM3U8: res.data.stream_data.file.includes('.m3u8'),
61 | });
62 |
63 | return this.sources;
64 | };
65 |
66 | private addSources = (source: any) => {
67 | this.sources.push({
68 | url: source.file,
69 | isM3U8: source.file.includes('.m3u8'),
70 | });
71 | };
72 | }
73 |
74 | export default StreamSB;
75 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import Fastify from 'fastify';
2 | import FastifyCors from '@fastify/cors';
3 | import Redis from 'ioredis';
4 | import gogoanime from './routes';
5 | import chalk from 'chalk';
6 |
7 | const fastify = Fastify({
8 | maxParamLength: 1000,
9 | logger: true,
10 | });
11 |
12 | export const redis =
13 | process.env.REDIS_HOST &&
14 | new Redis({
15 | host: process.env.REDIS_HOST,
16 | port: Number(process.env.REDIS_PORT),
17 | password: process.env.REDIS_PASSWORD,
18 | });
19 |
20 | (async () => {
21 | const PORT = 3000;
22 |
23 | await fastify.register(FastifyCors, {
24 | origin: '*',
25 | methods: 'GET',
26 | });
27 |
28 | console.log(chalk.green(`Starting server on port ${PORT}... 🚀`));
29 | if (!process.env.REDIS_HOST)
30 | console.warn(chalk.yellowBright('Redis not found. Cache disabled.'));
31 |
32 | await fastify.register(gogoanime, { prefix: '/' });
33 |
34 | try {
35 | fastify.get('*', (request, reply) => {
36 | reply.status(404).send({
37 | message: '',
38 | error: '404 Error: Page not found',
39 | });
40 | });
41 |
42 | fastify.listen({ port: PORT, host: '0.0.0.0' }, (e, address) => {
43 | if (e) throw e;
44 | console.log(`Server listening on ${address}`);
45 | });
46 | } catch (err: any) {
47 | fastify.log.error(err);
48 | process.exit(1);
49 | }
50 | })();
51 |
52 | // Export the handler function for use with serverless platforms
53 | export default async function handler(req: any, res: any) {
54 | await fastify.ready();
55 | fastify.server.emit('request', req, res);
56 | }
57 |
--------------------------------------------------------------------------------
/models/anime-parser.ts:
--------------------------------------------------------------------------------
1 | import { BaseParser, IAnimeInfo, ISource, IEpisodeServer } from '.';
2 |
3 | abstract class AnimeParser extends BaseParser {
4 | /**
5 | * if the provider has dub and it's avialable seperatly from sub set this to `true`
6 | */
7 | protected readonly isDubAvailableSeparately: boolean = false;
8 | /**
9 | * takes anime id
10 | *
11 | * returns anime info (including episodes)
12 | */
13 | abstract fetchAnimeInfo(animeId: string, ...args: any): Promise;
14 |
15 | /**
16 | * takes episode id
17 | *
18 | * returns episode sources (video links)
19 | */
20 | abstract fetchEpisodeSources(episodeId: string, ...args: any): Promise;
21 |
22 | /**
23 | * takes episode id
24 | *
25 | * returns episode servers (video links) available
26 | */
27 | abstract fetchEpisodeServers(episodeId: string): Promise;
28 | }
29 |
30 | export default AnimeParser;
31 |
--------------------------------------------------------------------------------
/models/base-parser.ts:
--------------------------------------------------------------------------------
1 | import { BaseProvider } from '.';
2 |
3 | abstract class BaseParser extends BaseProvider {
4 | /**
5 | * Search for anime/manga using the given query
6 | *
7 | * returns a promise resolving to a data object
8 | */
9 | abstract search(query: string, ...args: any[]): Promise;
10 | }
11 |
12 | export default BaseParser;
13 |
--------------------------------------------------------------------------------
/models/base-provider.ts:
--------------------------------------------------------------------------------
1 | import { IProviderStats } from '.';
2 | import Proxy from './proxy';
3 |
4 | abstract class BaseProvider extends Proxy {
5 | /**
6 | * Name of the provider
7 | */
8 | abstract readonly name: string;
9 |
10 | /**
11 | * The main URL of the provider
12 | */
13 | protected abstract readonly baseUrl: string;
14 |
15 | /**
16 | * Most providers are english based, but if the provider is not english based override this value.
17 | * must be in [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format
18 | */
19 | protected readonly languages: string[] | string = 'en';
20 |
21 | /**
22 | * override as `true` if the provider **only** supports NSFW content
23 | */
24 | readonly isNSFW: boolean = false;
25 |
26 | /**
27 | * Logo of the provider (used in the website) or `undefined` if not available. ***128x128px is preferred***\
28 | * Must be a valid URL (not a data URL)
29 | */
30 | protected readonly logo: string =
31 | 'https://png.pngtree.com/png-vector/20210221/ourmid/pngtree-error-404-not-found-neon-effect-png-image_2928214.jpg';
32 |
33 | /**
34 | * The class's path is determined by the provider's directory structure for example:\
35 | * MangaDex class path is `MANGA.MangaDex`. **(case sensitive)**
36 | */
37 | protected abstract readonly classPath: string;
38 |
39 | /**
40 | * override as `false` if the provider is **down** or **not working**
41 | */
42 | readonly isWorking: boolean = true;
43 |
44 | /**
45 | * returns provider stats
46 | */
47 | get toString(): IProviderStats {
48 | return {
49 | name: this.name,
50 | baseUrl: this.baseUrl,
51 | lang: this.languages,
52 | isNSFW: this.isNSFW,
53 | logo: this.logo,
54 | classPath: this.classPath,
55 | isWorking: this.isWorking,
56 | };
57 | }
58 | }
59 |
60 | export default BaseProvider;
61 |
--------------------------------------------------------------------------------
/models/index.ts:
--------------------------------------------------------------------------------
1 | import BaseProvider from './base-provider';
2 | import BaseParser from './base-parser';
3 | import AnimeParser from './anime-parser';
4 | import VideoExtractor from './video-extractor';
5 |
6 | import {
7 | IProviderStats,
8 | ISearch,
9 | IAnimeEpisode,
10 | IAnimeInfo,
11 | IAnimeResult,
12 | IEpisodeServer,
13 | IVideo,
14 | StreamingServers,
15 | MediaStatus,
16 | SubOrSub,
17 | IMangaChapter,
18 | IMangaChapterPage,
19 | ISource,
20 | ISubtitle,
21 | Intro,
22 | Genres,
23 | FuzzyDate,
24 | ITitle,
25 | MediaFormat,
26 | ProxyConfig,
27 | } from './types';
28 | export {
29 | BaseProvider,
30 | IProviderStats,
31 | BaseParser,
32 | AnimeParser,
33 | IAnimeEpisode,
34 | IAnimeInfo,
35 | IAnimeResult,
36 | IEpisodeServer,
37 | IVideo,
38 | VideoExtractor,
39 | StreamingServers,
40 | MediaStatus,
41 | SubOrSub,
42 | IMangaChapter,
43 | ISearch,
44 | IMangaChapterPage,
45 | ISource,
46 | ISubtitle,
47 | Intro,
48 | Genres,
49 | FuzzyDate,
50 | ITitle,
51 | MediaFormat,
52 | ProxyConfig,
53 | };
54 |
--------------------------------------------------------------------------------
/models/manga-parser.ts:
--------------------------------------------------------------------------------
1 | import { BaseParser, IMangaInfo, IMangaChapterPage } from '.';
2 |
3 | abstract class MangaParser extends BaseParser {
4 | /**
5 | * takes manga id
6 | *
7 | * returns manga info with chapters
8 | */
9 | abstract fetchMangaInfo(mangaId: string, ...args: any): Promise;
10 |
11 | /**
12 | * takes chapter id
13 | *
14 | * returns chapter (image links)
15 | */
16 | abstract fetchChapterPages(chapterId: string, ...args: any): Promise;
17 | }
18 |
19 | export default MangaParser;
20 |
--------------------------------------------------------------------------------
/models/proxy.ts:
--------------------------------------------------------------------------------
1 | import axios, {
2 | AxiosAdapter,
3 | AxiosInstance,
4 | AxiosHeaders,
5 | AxiosRequestHeaders,
6 | } from 'axios';
7 |
8 | import { ProxyConfig } from './types';
9 |
10 | export class Proxy {
11 | /**
12 | *
13 | * @param proxyConfig The proxy config (optional)
14 | * @param adapter The axios adapter (optional)
15 | */
16 | constructor(
17 | protected proxyConfig?: ProxyConfig,
18 | protected adapter?: AxiosAdapter,
19 | ) {
20 | this.client = axios.create();
21 |
22 | if (proxyConfig) this.setProxy(proxyConfig);
23 | if (adapter) this.setAxiosAdapter(adapter);
24 | }
25 | private validUrl = /^https?:\/\/.+/;
26 | /**
27 | * Set or Change the proxy config
28 | */
29 | setProxy(proxyConfig: ProxyConfig) {
30 | if (!proxyConfig?.url) return;
31 |
32 | if (typeof proxyConfig?.url === 'string')
33 | if (!this.validUrl.test(proxyConfig.url)) throw new Error('Proxy URL is invalid!');
34 |
35 | if (Array.isArray(proxyConfig?.url)) {
36 | for (const [i, url] of this.toMap(proxyConfig.url))
37 | if (!this.validUrl.test(url))
38 | throw new Error(`Proxy URL at index ${i} is invalid!`);
39 |
40 | this.rotateProxy({ ...proxyConfig, urls: proxyConfig.url });
41 | }
42 |
43 | this.client.interceptors.request.use((config) => {
44 | const headers = new AxiosHeaders();
45 | headers.set('x-api-key', proxyConfig?.key ?? '');
46 |
47 | if (proxyConfig?.url) {
48 | config.headers = headers;
49 | config.url = `${proxyConfig.url}${config?.url ? config?.url : ''}`;
50 | }
51 |
52 | if (config?.url?.includes('anify')) {
53 | headers.set('User-Agent', 'animetize');
54 | config.headers = headers;
55 | }
56 |
57 | return config;
58 | });
59 | }
60 |
61 | /**
62 | * Set or Change the axios adapter
63 | */
64 | setAxiosAdapter(adapter: AxiosAdapter) {
65 | this.client.defaults.adapter = adapter;
66 | }
67 | private rotateProxy = (proxy: Omit & { urls: string[] }) => {
68 | setInterval(() => {
69 | const url = proxy.urls.shift();
70 | if (url) proxy.urls.push(url);
71 |
72 | this.setProxy({ url: proxy.urls[0], key: proxy.key });
73 | }, proxy?.rotateInterval ?? 5000);
74 | };
75 |
76 | private toMap = (arr: T[]): [number, T][] => arr.map((v, i) => [i, v]);
77 |
78 | protected client: AxiosInstance;
79 | }
80 |
81 | export default Proxy;
82 |
--------------------------------------------------------------------------------
/models/types.ts:
--------------------------------------------------------------------------------
1 | export interface IProviderStats {
2 | name: string;
3 | baseUrl: string;
4 | lang: string[] | string;
5 | isNSFW: boolean;
6 | logo: string;
7 | classPath: string;
8 | isWorking: boolean;
9 | }
10 |
11 | export interface ITitle {
12 | romaji?: string;
13 | english?: string;
14 | native?: string;
15 | userPreferred?: string;
16 | }
17 |
18 | export interface IAnimeResult {
19 | id: string;
20 | title: string | ITitle;
21 | url?: string;
22 | image?: string;
23 | imageHash?: string;
24 | cover?: string;
25 | coverHash?: string;
26 | status?: MediaStatus;
27 | rating?: number;
28 | type?: MediaFormat;
29 | releaseDate?: string;
30 | [x: string]: any; // other fields
31 | }
32 |
33 | export interface ISearch {
34 | currentPage?: number;
35 | hasNextPage?: boolean;
36 | totalPages?: number;
37 | /**
38 | * total results must include results from all pages
39 | */
40 | totalResults?: number;
41 | results: T[];
42 | }
43 |
44 | export interface Trailer {
45 | id: string;
46 | site?: string;
47 | thumbnail?: string;
48 | thumbnailHash?: string | null;
49 | }
50 |
51 | export interface FuzzyDate {
52 | year?: number;
53 | month?: number;
54 | day?: number;
55 | }
56 |
57 | export enum MediaFormat {
58 | TV = 'TV',
59 | TV_SHORT = 'TV_SHORT',
60 | MOVIE = 'MOVIE',
61 | SPECIAL = 'SPECIAL',
62 | OVA = 'OVA',
63 | ONA = 'ONA',
64 | MUSIC = 'MUSIC',
65 | MANGA = 'MANGA',
66 | NOVEL = 'NOVEL',
67 | ONE_SHOT = 'ONE_SHOT',
68 | }
69 |
70 | export interface IAnimeInfo extends IAnimeResult {
71 | malId?: number | string;
72 | genres?: string[];
73 | description?: string;
74 | status?: MediaStatus;
75 | totalEpisodes?: number;
76 | /**
77 | * @deprecated use `hasSub` or `hasDub` instead
78 | */
79 | subOrDub?: SubOrSub;
80 | hasSub?: boolean;
81 | hasDub?: boolean;
82 | synonyms?: string[];
83 | /**
84 | * two letter representation of coutnry: e.g JP for japan
85 | */
86 | countryOfOrigin?: string;
87 | isAdult?: boolean;
88 | isLicensed?: boolean;
89 | /**
90 | * `FALL`, `WINTER`, `SPRING`, `SUMMER`
91 | */
92 | season?: string;
93 | studios?: string[];
94 | color?: string;
95 | cover?: string;
96 | trailer?: Trailer;
97 | episodes?: IAnimeEpisode[];
98 | startDate?: FuzzyDate;
99 | endDate?: FuzzyDate;
100 | recommendations?: IAnimeResult[];
101 | relations?: IAnimeResult[];
102 | }
103 |
104 | export interface IAnimeEpisodeV2 {
105 | [x: string]: {
106 | id: string;
107 | season_number: number;
108 | title: string;
109 | image: string;
110 | imageHash: string;
111 | description: string;
112 | releaseDate: string;
113 | isHD: boolean;
114 | isAdult: boolean;
115 | isDubbed: boolean;
116 | isSubbed: boolean;
117 | duration: number;
118 | }[];
119 | }
120 |
121 | export interface IAnimeEpisode {
122 | id: string;
123 | number: number;
124 | title?: string;
125 | description?: string;
126 | isFiller?: boolean;
127 | url?: string;
128 | image?: string;
129 | imageHash?: string;
130 | releaseDate?: string;
131 | [x: string]: unknown; // other fields
132 | }
133 |
134 | export interface IEpisodeServer {
135 | name: string;
136 | url: string;
137 | }
138 |
139 | export interface IVideo {
140 | /**
141 | * The **MAIN URL** of the video provider that should take you to the video
142 | */
143 | url: string;
144 | /**
145 | * The Quality of the video should include the `p` suffix
146 | */
147 | quality?: string;
148 | /**
149 | * make sure to set this to `true` if the video is hls
150 | */
151 | isM3U8?: boolean;
152 | /**
153 | * set this to `true` if the video is dash (mpd)
154 | */
155 | isDASH?: boolean;
156 | /**
157 | * size of the video in **bytes**
158 | */
159 | size?: number;
160 | [x: string]: unknown; // other fields
161 | }
162 |
163 | export enum StreamingServers {
164 | GogoCDN = 'gogocdn',
165 | StreamSB = 'streamsb',
166 | StreamTape = 'streamtape',
167 | // same as vizcloud
168 | VidStreaming = 'vidstreaming',
169 | }
170 |
171 | export enum MediaStatus {
172 | ONGOING = 'Ongoing',
173 | COMPLETED = 'Completed',
174 | HIATUS = 'Hiatus',
175 | CANCELLED = 'Cancelled',
176 | NOT_YET_AIRED = 'Not yet aired',
177 | UNKNOWN = 'Unknown',
178 | }
179 |
180 | export enum SubOrSub {
181 | SUB = 'sub',
182 | DUB = 'dub',
183 | BOTH = 'both',
184 | }
185 |
186 | export interface IMangaChapter {
187 | id: string;
188 | title: string;
189 | volume?: number;
190 | pages?: number;
191 | releaseDate?: string;
192 | [x: string]: unknown; // other fields
193 | }
194 |
195 | export interface IMangaChapterPage {
196 | img: string;
197 | page: number;
198 | [x: string]: unknown; // other fields
199 | }
200 |
201 | export interface ISubtitle {
202 | /**
203 | * The id of the subtitle. **not** required
204 | */
205 | id?: string;
206 | /**
207 | * The **url** that should take you to the subtitle **directly**.
208 | */
209 | url: string;
210 | /**
211 | * The language of the subtitle
212 | */
213 | lang: string;
214 | }
215 |
216 | /**
217 | * The start, and the end of the intro or opening in seconds.
218 | */
219 | export interface Intro {
220 | start: number;
221 | end: number;
222 | }
223 |
224 | export interface ISource {
225 | headers?: { [k: string]: string };
226 | intro?: Intro;
227 | outro?: Intro;
228 | subtitles?: ISubtitle[];
229 | sources: IVideo[];
230 | download?: string;
231 | embedURL?: string;
232 | }
233 |
234 | export enum Genres {
235 | ACTION = 'Action',
236 | ADVENTURE = 'Adventure',
237 | CARS = 'Cars',
238 | COMEDY = 'Comedy',
239 | DRAMA = 'Drama',
240 | FANTASY = 'Fantasy',
241 | HORROR = 'Horror',
242 | MAHOU_SHOUJO = 'Mahou Shoujo',
243 | MECHA = 'Mecha',
244 | MUSIC = 'Music',
245 | MYSTERY = 'Mystery',
246 | PSYCHOLOGICAL = 'Psychological',
247 | ROMANCE = 'Romance',
248 | SCI_FI = 'Sci-Fi',
249 | SLICE_OF_LIFE = 'Slice of Life',
250 | SPORTS = 'Sports',
251 | SUPERNATURAL = 'Supernatural',
252 | THRILLER = 'Thriller',
253 | }
254 |
255 | export interface ProxyConfig {
256 | /**
257 | * The proxy URL
258 | * @example https://proxy.com
259 | **/
260 | url: string | string[];
261 | /**
262 | * X-API-Key header value (if any)
263 | **/
264 | key?: string;
265 | /**
266 | * The proxy rotation interval in milliseconds. (default: 5000)
267 | */
268 | rotateInterval?: number;
269 | }
270 |
--------------------------------------------------------------------------------
/models/video-extractor.ts:
--------------------------------------------------------------------------------
1 | import { IVideo, ISource } from '.';
2 | import Proxy from './proxy';
3 |
4 | abstract class VideoExtractor extends Proxy {
5 | /**
6 | * The server name of the video provider
7 | */
8 | protected abstract serverName: string;
9 |
10 | /**
11 | * list of videos available
12 | */
13 | protected abstract sources: IVideo[];
14 |
15 | /**
16 | * takes video link
17 | *
18 | * returns video sources (video links) available
19 | */
20 | protected abstract extract(videoUrl: URL, ...args: any): Promise;
21 | }
22 |
23 | export default VideoExtractor;
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "animetize",
3 | "version": "0.0.1",
4 | "main": "main.ts",
5 | "scripts": {
6 | "start": "ts-node main.ts",
7 | "dev": "nodemon main.ts",
8 | "build": "tsc",
9 | "lint": "prettier --write \"**/*.ts\""
10 | },
11 | "dependencies": {
12 | "@fastify/cors": "^10.0.0",
13 | "@types/fastify-cors": "^2.1.0",
14 | "@types/node": "^22.0.0",
15 | "@types/ws": "^8.5.3",
16 | "ascii-url-encoder": "^1.2.0",
17 | "axios": "^1.0.0",
18 | "chalk": "5.4.1",
19 | "cheerio": "1.0.0",
20 | "crypto-js": "^4.2.0",
21 | "dotenv": "^16.0.3",
22 | "fastify": "^5.0.0",
23 | "fastify-cors": "^6.1.0",
24 | "ioredis": "^5.4.1"
25 | },
26 | "devDependencies": {
27 | "@types/crypto-js": "^4.2.2",
28 | "nodemon": "3.0.1",
29 | "prettier": "^3.0.0",
30 | "ts-node": "^10.9.2",
31 | "typescript": "^5.3.3"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/providers/gogoanime.ts:
--------------------------------------------------------------------------------
1 | import { load } from 'cheerio';
2 |
3 | import {
4 | AnimeParser,
5 | ISearch,
6 | IAnimeInfo,
7 | IEpisodeServer,
8 | StreamingServers,
9 | MediaStatus,
10 | SubOrSub,
11 | IAnimeResult,
12 | ISource,
13 | MediaFormat,
14 | } from '../models';
15 | import { GogoCDN, StreamSB } from '../extractors';
16 |
17 | class Gogoanime extends AnimeParser {
18 | override readonly name = 'Gogoanime';
19 | protected override baseUrl = 'https://gogoanime3.co';
20 | protected override logo =
21 | 'https://play-lh.googleusercontent.com/MaGEiAEhNHAJXcXKzqTNgxqRmhuKB1rCUgb15UrN_mWUNRnLpO5T1qja64oRasO7mn0';
22 | protected override classPath = 'ANIME.Gogoanime';
23 | private readonly ajaxUrl = 'https://ajax.gogocdn.net/ajax';
24 |
25 | /**
26 | *
27 | * @param query search query string
28 | * @param page page number (default 1) (optional)
29 | */
30 | override search = async (
31 | query: string,
32 | page: number = 1,
33 | ): Promise> => {
34 | const searchResult: ISearch = {
35 | currentPage: page,
36 | hasNextPage: false,
37 | results: [],
38 | };
39 | try {
40 | const res = await this.client.get(
41 | `${this.baseUrl}/filter.html?keyword=${encodeURIComponent(query)}&page=${page}`,
42 | );
43 |
44 | const $ = load(res.data);
45 |
46 | searchResult.hasNextPage =
47 | $('div.anime_name.new_series > div > div > ul > li.selected').next().length > 0;
48 |
49 | $('div.last_episodes > ul > li').each((i, el) => {
50 | searchResult.results.push({
51 | id: $(el).find('p.name > a').attr('href')?.split('/')[2]!,
52 | title: $(el).find('p.name > a').attr('title')!,
53 | url: `${this.baseUrl}/${$(el).find('p.name > a').attr('href')}`,
54 | image: $(el).find('div > a > img').attr('src'),
55 | releaseDate: $(el).find('p.released').text().trim(),
56 | subOrDub: $(el).find('p.name > a').text().toLowerCase().includes('dub')
57 | ? SubOrSub.DUB
58 | : SubOrSub.SUB,
59 | });
60 | });
61 |
62 | return searchResult;
63 | } catch (err) {
64 | throw new Error((err as Error).message);
65 | }
66 | };
67 |
68 | /**
69 | *
70 | * @param id anime id
71 | */
72 | override fetchAnimeInfo = async (id: string): Promise => {
73 | if (!id.includes('gogoanime')) id = `${this.baseUrl}/category/${id}`;
74 |
75 | const animeInfo: IAnimeInfo = {
76 | id: '',
77 | title: '',
78 | url: '',
79 | genres: [],
80 | totalEpisodes: 0,
81 | };
82 | try {
83 | const res = await this.client.get(id);
84 |
85 | const $ = load(res.data);
86 |
87 | animeInfo.id = new URL(id).pathname.split('/')[2];
88 | animeInfo.title = $(
89 | 'section.content_left > div.main_body > div:nth-child(2) > div.anime_info_body_bg > h1',
90 | )
91 | .text()
92 | .trim();
93 | animeInfo.url = id;
94 | animeInfo.image = $('div.anime_info_body_bg > img').attr('src');
95 | animeInfo.releaseDate = $('div.anime_info_body_bg > p:nth-child(8)')
96 | .text()
97 | .trim()
98 | .split('Released: ')[1];
99 | animeInfo.description = $('div.anime_info_body_bg > div:nth-child(6)')
100 | .text()
101 | .trim()
102 | .replace('Plot Summary: ', '');
103 |
104 | animeInfo.subOrDub = animeInfo.title.toLowerCase().includes('dub')
105 | ? SubOrSub.DUB
106 | : SubOrSub.SUB;
107 |
108 | animeInfo.type = $('div.anime_info_body_bg > p:nth-child(4) > a')
109 | .text()
110 | .trim()
111 | .toUpperCase() as MediaFormat;
112 |
113 | animeInfo.status = MediaStatus.UNKNOWN;
114 |
115 | switch ($('div.anime_info_body_bg > p:nth-child(9) > a').text().trim()) {
116 | case 'Ongoing':
117 | animeInfo.status = MediaStatus.ONGOING;
118 | break;
119 | case 'Completed':
120 | animeInfo.status = MediaStatus.COMPLETED;
121 | break;
122 | case 'Upcoming':
123 | animeInfo.status = MediaStatus.NOT_YET_AIRED;
124 | break;
125 | default:
126 | animeInfo.status = MediaStatus.UNKNOWN;
127 | break;
128 | }
129 | animeInfo.otherName = $('div.anime_info_body_bg > p:nth-child(10)')
130 | .text()
131 | .replace('Other name: ', '')
132 | .replace(/;/g, ',');
133 |
134 | $('div.anime_info_body_bg > p:nth-child(7) > a').each((i, el) => {
135 | animeInfo.genres?.push($(el).attr('title')!.toString());
136 | });
137 |
138 | const ep_start = $('#episode_page > li').first().find('a').attr('ep_start');
139 | const ep_end = $('#episode_page > li').last().find('a').attr('ep_end');
140 | const movie_id = $('#movie_id').attr('value');
141 | const alias = $('#alias_anime').attr('value');
142 |
143 | const html = await this.client.get(
144 | `${this.ajaxUrl}/load-list-episode?ep_start=${ep_start}&ep_end=${ep_end}&id=${movie_id}&default_ep=${0}&alias=${alias}`,
145 | );
146 | const $$ = load(html.data);
147 |
148 | animeInfo.episodes = [];
149 | $$('#episode_related > li').each((i, el) => {
150 | animeInfo.episodes?.push({
151 | id: $(el).find('a').attr('href')?.split('/')[1]!,
152 | number: parseFloat($(el).find(`div.name`).text().replace('EP ', '')),
153 | url: `${this.baseUrl}/${$(el).find(`a`).attr('href')?.trim()}`,
154 | });
155 | });
156 | animeInfo.episodes = animeInfo.episodes.reverse();
157 |
158 | animeInfo.totalEpisodes = parseInt(ep_end ?? '0');
159 |
160 | return animeInfo;
161 | } catch (err) {
162 | throw new Error(`failed to fetch anime info: ${err}`);
163 | }
164 | };
165 |
166 | /**
167 | *
168 | * @param episodeId episode id
169 | * @param server server type (default 'GogoCDN') (optional)
170 | */
171 | override fetchEpisodeSources = async (
172 | episodeId: string,
173 | server: StreamingServers = StreamingServers.VidStreaming,
174 | ): Promise => {
175 | if (episodeId.startsWith('http')) {
176 | const serverUrl = new URL(episodeId);
177 | switch (server) {
178 | case StreamingServers.GogoCDN:
179 | return {
180 | headers: { Referer: serverUrl.href },
181 | sources: await new GogoCDN(this.proxyConfig, this.adapter).extract(serverUrl),
182 | download: `https://gogohd.net/download${serverUrl.search}`,
183 | };
184 | case StreamingServers.StreamSB:
185 | return {
186 | headers: {
187 | Referer: serverUrl.href,
188 | watchsb: 'streamsb',
189 | 'User-Agent':
190 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36',
191 | },
192 | sources: await new StreamSB(this.proxyConfig, this.adapter).extract(
193 | serverUrl,
194 | ),
195 | download: `https://gogohd.net/download${serverUrl.search}`,
196 | };
197 | default:
198 | return {
199 | headers: { Referer: serverUrl.href },
200 | sources: await new GogoCDN(this.proxyConfig, this.adapter).extract(serverUrl),
201 | download: `https://gogohd.net/download${serverUrl.search}`,
202 | };
203 | }
204 | }
205 |
206 | try {
207 | const res = await this.client.get(`${this.baseUrl}/${episodeId}`);
208 |
209 | const $ = load(res.data);
210 |
211 | let serverUrl: URL;
212 |
213 | switch (server) {
214 | case StreamingServers.GogoCDN:
215 | serverUrl = new URL(`${$('#load_anime > div > div > iframe').attr('src')}`);
216 | break;
217 | case StreamingServers.VidStreaming:
218 | serverUrl = new URL(
219 | `${$('div.anime_video_body > div.anime_muti_link > ul > li.vidcdn > a').attr('data-video')}`,
220 | );
221 | break;
222 | case StreamingServers.StreamSB:
223 | serverUrl = new URL(
224 | $('div.anime_video_body > div.anime_muti_link > ul > li.streamsb > a').attr(
225 | 'data-video',
226 | )!,
227 | );
228 | break;
229 | default:
230 | serverUrl = new URL(`${$('#load_anime > div > div > iframe').attr('src')}`);
231 | break;
232 | }
233 |
234 | return await this.fetchEpisodeSources(serverUrl.href, server);
235 | } catch (err) {
236 | console.log(err);
237 | throw new Error('Episode not found.');
238 | }
239 | };
240 |
241 | /**
242 | *
243 | * @param episodeId episode link or episode id
244 | */
245 | override fetchEpisodeServers = async (episodeId: string): Promise => {
246 | try {
247 | if (!episodeId.startsWith(this.baseUrl)) episodeId = `${this.baseUrl}/${episodeId}`;
248 |
249 | const res = await this.client.get(episodeId);
250 |
251 | const $ = load(res.data);
252 |
253 | const servers: IEpisodeServer[] = [];
254 |
255 | $('div.anime_video_body > div.anime_muti_link > ul > li').each((i, el) => {
256 | let url = $(el).find('a').attr('data-video');
257 | if (!url?.startsWith('http')) url = `https:${url}`;
258 |
259 | servers.push({
260 | name: $(el).find('a').text().replace('Choose this server', '').trim(),
261 | url: url,
262 | });
263 | });
264 |
265 | return servers;
266 | } catch (err) {
267 | throw new Error('Episode not found.');
268 | }
269 | };
270 | /**
271 | *
272 | * @param episodeId episode link or episode id
273 | */
274 | fetchAnimeIdFromEpisodeId = async (episodeId: string): Promise => {
275 | try {
276 | if (!episodeId.startsWith(this.baseUrl)) episodeId = `${this.baseUrl}/${episodeId}`;
277 |
278 | const res = await this.client.get(episodeId);
279 |
280 | const $ = load(res.data);
281 |
282 | return (
283 | $(
284 | '#wrapper_bg > section > section.content_left > div:nth-child(1) > div.anime_video_body > div.anime_video_body_cate > div.anime-info > a',
285 | ).attr('href') as string
286 | ).split('/')[2];
287 | } catch (err) {
288 | throw new Error('Episode not found.');
289 | }
290 | };
291 | /**
292 | * @param page page number (optional)
293 | * @param type type of media. (optional) (default `1`) `1`: Japanese with subtitles, `2`: english/dub with no subtitles, `3`: chinese with english subtitles
294 | */
295 | fetchRecentEpisodes = async (
296 | page: number = 1,
297 | type: number = 1,
298 | ): Promise> => {
299 | try {
300 | const res = await this.client.get(
301 | `${this.ajaxUrl}/page-recent-release.html?page=${page}&type=${type}`,
302 | );
303 |
304 | const $ = load(res.data);
305 |
306 | const recentEpisodes: IAnimeResult[] = [];
307 |
308 | $('div.last_episodes.loaddub > ul > li').each((i, el) => {
309 | recentEpisodes.push({
310 | id: $(el).find('a').attr('href')?.split('/')[1]?.split('-episode')[0]!,
311 | episodeId: $(el).find('a').attr('href')?.split('/')[1]!,
312 | episodeNumber: parseFloat(
313 | $(el).find('p.episode').text().replace('Episode ', ''),
314 | ),
315 | title: $(el).find('p.name > a').attr('title')!,
316 | image: $(el).find('div > a > img').attr('src'),
317 | url: `${this.baseUrl}${$(el).find('a').attr('href')?.trim()}`,
318 | });
319 | });
320 |
321 | const hasNextPage = !$('div.anime_name_pagination.intro > div > ul > li')
322 | .last()
323 | .hasClass('selected');
324 |
325 | return {
326 | currentPage: page,
327 | hasNextPage: hasNextPage,
328 | results: recentEpisodes,
329 | };
330 | } catch (err) {
331 | throw new Error('Something went wrong. Please try again later.');
332 | }
333 | };
334 |
335 | fetchGenreInfo = async (
336 | genre: string,
337 | page: number = 1,
338 | ): Promise> => {
339 | try {
340 | const res = await this.client.get(`${this.baseUrl}/genre/${genre}?page=${page}`);
341 |
342 | const $ = load(res.data);
343 |
344 | const genreInfo: IAnimeResult[] = [];
345 |
346 | $('div.last_episodes > ul > li').each((i, elem) => {
347 | genreInfo.push({
348 | id: $(elem).find('p.name > a').attr('href')?.split('/')[2] as string,
349 | title: $(elem).find('p.name > a').attr('title') as string,
350 | image: $(elem).find('div > a > img').attr('src'),
351 | released: $(elem).find('p.released').text().replace('Released: ', '').trim(),
352 | url: this.baseUrl + '/' + $(elem).find('p.name > a').attr('href'),
353 | });
354 | });
355 |
356 | const paginatorDom = $('div.anime_name_pagination > div > ul > li');
357 | const hasNextPage =
358 | paginatorDom.length > 0 && !paginatorDom.last().hasClass('selected');
359 | return {
360 | currentPage: page,
361 | hasNextPage: hasNextPage,
362 | results: genreInfo,
363 | };
364 | } catch (err) {
365 | throw new Error('Something went wrong. Please try again later.');
366 | }
367 | };
368 |
369 | fetchTopAiring = async (page: number = 1): Promise> => {
370 | try {
371 | const res = await this.client.get(
372 | `${this.ajaxUrl}/page-recent-release-ongoing.html?page=${page}`,
373 | );
374 |
375 | const $ = load(res.data);
376 |
377 | const topAiring: IAnimeResult[] = [];
378 |
379 | $('div.added_series_body.popular > ul > li').each((i, el) => {
380 | topAiring.push({
381 | id: $(el).find('a:nth-child(1)').attr('href')?.split('/')[2]!,
382 | title: $(el).find('a:nth-child(1)').attr('title')!,
383 | image: $(el)
384 | .find('a:nth-child(1) > div')
385 | .attr('style')
386 | ?.match('(https?://.*.(?:png|jpg))')![0],
387 | url: `${this.baseUrl}${$(el).find('a:nth-child(1)').attr('href')}`,
388 | genres: $(el)
389 | .find('p.genres > a')
390 | .map((i, el) => $(el).attr('title'))
391 | .get(),
392 | });
393 | });
394 |
395 | const hasNextPage = !$('div.anime_name.comedy > div > div > ul > li')
396 | .last()
397 | .hasClass('selected');
398 |
399 | return {
400 | currentPage: page,
401 | hasNextPage: hasNextPage,
402 | results: topAiring,
403 | };
404 | } catch (err) {
405 | throw new Error('Something went wrong. Please try again later.');
406 | }
407 | };
408 |
409 | fetchDirectDownloadLink = async (downloadUrl: string, captchaToken?: string): Promise<{ source: string | undefined; link: string | undefined }[]> => {
410 | const downloadLinks: { source: string | undefined; link: string | undefined }[] = [];
411 |
412 | const baseUrl = downloadUrl.split('?')[0];
413 | const idParam = downloadUrl.match(/[?&]id=([^&]+)/);
414 | const animeID = idParam ? idParam[1] : null;
415 | if (!captchaToken)
416 | captchaToken = '03AFcWeA5zy7DBK82U_tctVKelJ6L2duTWac5at2zXjHLX8XqUm8tI6NKWMxGd2gjh1vi2hnEyRhVgbMhdb9WjexRsJkxTt-C-_iIIZ5yC3E5I19G5Q0buSTcIQIZS6tskrz-mDn-d37aWxAJtqbg0Yoo1XsdVc5Yf4sB-9iQxQK-W_9YLep_QaAz8uL17gMMlCz5WZM3dbBEEGmk_qPbJu_pZ8kk-lFPDzd6iBobcpyIDRZgTgD4bYUnby5WZc11i00mrRiRS3m-qSY0lprGaBqoyY1BbRkQZ25AGPp5al4kSwBZqpcVgLrs3bjdo8XVWAe73_XLa8HhqLWbz_m5Ebyl5F9awwL7w4qikGj-AK7v2G8pgjT22kDLIeenQ_ss4jYpmSzgnuTItur9pZVzpPkpqs4mzr6y274AmJjzppRTDH4VFtta_E02-R7Hc1rUD2kCYt9BqsD7kDjmetnvLtBm97q5XgBS8rQfeH4P-xqiTAsJwXlcrPybSjnwPEptqYCPX5St_BSj4NQfSuzZowXu_qKsP4hAaE9L2W36MvqePPlEm6LChBT3tnqUwcEYNe5k7lkAAbunxx8q_X5Q3iEdcFqt9_0GWHebRBd5abEbjbmoqqCoQeZt7AUvkXCRfBDne-bf25ypyTtwgyuvYMYXau3zGUjgPUO9WIotZwyKyrYmjsZJ7TiM';
417 |
418 | let res = null;
419 | try {
420 | res = await this.client.get(`${baseUrl}?id=${animeID}&captcha_v3=${captchaToken}`);
421 | } catch (err) {
422 | throw new Error('Something went wrong. Please try again later.');
423 | }
424 | try {
425 | const $ = load(res.data);
426 | $('.dowload').each((_index, element) => {
427 | const link = $(element).find('a');
428 | if (link.attr('target') != '_blank') {
429 | downloadLinks.push({ source: link.text(), link: link.attr('href') }!);
430 | }
431 | });
432 | return downloadLinks;
433 | } catch (err) {
434 | throw new Error('Something went wrong. Please try again later.');
435 | }
436 | };
437 |
438 | fetchRecentMovies = async (page: number = 1): Promise> => {
439 | try {
440 | const res = await this.client.get(
441 | `${this.baseUrl}/anime-movies.html?aph&page=${page}`,
442 | );
443 |
444 | const $ = load(res.data);
445 |
446 | const recentMovies: IAnimeResult[] = [];
447 |
448 | $('div.last_episodes > ul > li').each((i, el) => {
449 | const a = $(el).find('p.name > a');
450 | const pRelease = $(el).find('p.released');
451 | const pName = $(el).find('p.name > a');
452 |
453 | recentMovies.push({
454 | id: a.attr('href')?.replace(`/category/`, '')!,
455 | title: pName.attr('title')!,
456 | releaseDate: pRelease.text().replace('Released: ', '').trim(),
457 | image: $(el).find('div > a > img').attr('src'),
458 | url: `${this.baseUrl}${a.attr('href')}`,
459 | });
460 | });
461 |
462 | const hasNextPage = !$('div.anime_name.anime_movies > div > div > ul > li')
463 | .last()
464 | .hasClass('selected');
465 |
466 | return {
467 | currentPage: page,
468 | hasNextPage: hasNextPage,
469 | results: recentMovies,
470 | };
471 | } catch (err) {
472 | console.log(err);
473 | throw new Error('Something went wrong. Please try again later.');
474 | }
475 | };
476 |
477 | fetchPopular = async (page: number = 1): Promise> => {
478 | try {
479 | const res = await this.client.get(`${this.baseUrl}/popular.html?page=${page}`);
480 |
481 | const $ = load(res.data);
482 |
483 | const recentMovies: IAnimeResult[] = [];
484 |
485 | $('div.last_episodes > ul > li').each((i, el) => {
486 | const a = $(el).find('p.name > a');
487 | const pRelease = $(el).find('p.released');
488 | const pName = $(el).find('p.name > a');
489 |
490 | recentMovies.push({
491 | id: a.attr('href')?.replace(`/category/`, '')!,
492 | title: pName.attr('title')!,
493 | releaseDate: pRelease.text().replace('Released: ', '').trim(),
494 | image: $(el).find('div > a > img').attr('src'),
495 | url: `${this.baseUrl}${a.attr('href')}`,
496 | });
497 | });
498 |
499 | const hasNextPage = !$('div.anime_name.anime_movies > div > div > ul > li')
500 | .last()
501 | .hasClass('selected');
502 |
503 | return {
504 | currentPage: page,
505 | hasNextPage: hasNextPage,
506 | results: recentMovies,
507 | };
508 | } catch (err) {
509 | console.log(err);
510 | throw new Error('Something went wrong. Please try again later.');
511 | }
512 | };
513 |
514 | fetchGenreList = async (): Promise<
515 | { id: string | undefined; title: string | undefined }[]
516 | > => {
517 | const genres: { id: string | undefined; title: string | undefined }[] = [];
518 | let res = null;
519 | try {
520 | res = await this.client.get(`${this.baseUrl}/home.html`);
521 | } catch (err) {
522 | try {
523 | res = await this.client.get(`${this.baseUrl}/`);
524 | } catch (error) {
525 | throw new Error('Something went wrong. Please try again later.');
526 | }
527 | }
528 | try {
529 | const $ = load(res.data);
530 | $('nav.menu_series.genre.right > ul > li').each((_index, element) => {
531 | const genre = $(element).find('a');
532 | genres.push(
533 | { id: genre.attr('href')?.replace('/genre/', ''), title: genre.attr('title') }!,
534 | );
535 | });
536 | return genres;
537 | } catch (err) {
538 | throw new Error('Something went wrong. Please try again later.');
539 | }
540 | };
541 |
542 | fetchAnimeList = async (page: number = 1): Promise> => {
543 | const animeList: IAnimeResult[] = [];
544 | let res = null;
545 | try {
546 | res = await this.client.get(`${this.baseUrl}/anime-list.html?page=${page}`);
547 | const $ = load(res.data);
548 | $('.anime_list_body .listing li').each((_index, element) => {
549 | const img = $('div', $(element).attr('title')!);
550 | const a = $(element).find('a');
551 | animeList.push({
552 | id: a.attr('href')?.replace(`/category/`, '')!,
553 | title: a.text(),
554 | image: $(img).find('img').attr('src'),
555 | url: `${this.baseUrl}${a.attr('href')}`,
556 | });
557 | });
558 | const hasNextPage = !$('div.anime_name.anime_list > div > div > ul > li')
559 | .last()
560 | .hasClass('selected');
561 | return {
562 | currentPage: page,
563 | hasNextPage: hasNextPage,
564 | results: animeList,
565 | };
566 | } catch (err) {
567 | throw new Error('Something went wrong. Please try again later.');
568 | }
569 | };
570 | }
571 |
572 | // (async () => {
573 | // const gogo = new Gogoanime();
574 | // const search = await gogo.fetchEpisodeSources('jigokuraku-dub-episode-1');
575 | // console.log(search);
576 | // })();
577 |
578 | export default Gogoanime;
579 |
--------------------------------------------------------------------------------
/routes/gogoanime.ts:
--------------------------------------------------------------------------------
1 | import { FastifyRequest, FastifyReply, FastifyInstance, RegisterOptions } from 'fastify';
2 | import Gogoanime from '../providers/gogoanime';
3 | import { StreamingServers } from '../models';
4 | import cache from '../utils/cache';
5 | import { redis } from '../main';
6 | import { Redis } from 'ioredis';
7 |
8 | const routes = async (fastify: FastifyInstance, options: RegisterOptions) => {
9 | const gogoanime = new Gogoanime();
10 | const redisCacheTime = 60 * 60;
11 | const redisPrefix = 'gogoanime:';
12 |
13 | fastify.get('/', (_, rp) => {
14 | rp.status(200).send({
15 | intro:
16 | "Welcome to the gogoanime provider: check out the provider's website @ https://www1.gogoanime.bid/",
17 | routes: [
18 | '/:query',
19 | '/info/:id',
20 | '/watch/:episodeId',
21 | '/servers/:episodeId',
22 | '/genre/:genre',
23 | '/genre/list',
24 | '/top-airing',
25 | '/movies',
26 | '/popular',
27 | '/recent-episodes',
28 | '/anime-list',
29 | '/download',
30 | ],
31 | documentation: 'https://animetize-docs.vercel.app/',
32 | });
33 | });
34 |
35 | fastify.get('/:query', async (request: FastifyRequest, reply: FastifyReply) => {
36 | const query = (request.params as { query: string }).query;
37 | const page = (request.query as { page: number }).page || 1;
38 |
39 | const res = redis
40 | ? await cache.fetch(
41 | redis as Redis,
42 | `${redisPrefix}search;${page};${query}`,
43 | async () => await gogoanime.search(query, page),
44 | redisCacheTime,
45 | )
46 | : await gogoanime.search(query, page);
47 |
48 | reply.status(200).send(res);
49 | });
50 |
51 | fastify.get('/info/:id', async (request: FastifyRequest, reply: FastifyReply) => {
52 | const id = decodeURIComponent((request.params as { id: string }).id);
53 |
54 | try {
55 | const res = redis
56 | ? await cache.fetch(
57 | redis as Redis,
58 | `${redisPrefix}info;${id}`,
59 | async () =>
60 | await gogoanime
61 | .fetchAnimeInfo(id)
62 | .catch((err) => reply.status(404).send({ message: err })),
63 | redisCacheTime,
64 | )
65 | : await gogoanime
66 | .fetchAnimeInfo(id)
67 | .catch((err) => reply.status(404).send({ message: err }));
68 |
69 | reply.status(200).send(res);
70 | } catch (err) {
71 | reply
72 | .status(500)
73 | .send({ message: 'Something went wrong. Please try again later.' });
74 | }
75 | });
76 |
77 | fastify.get('/genre/:genre', async (request: FastifyRequest, reply: FastifyReply) => {
78 | const genre = (request.params as { genre: string }).genre;
79 | const page = (request.query as { page: number }).page ?? 1;
80 |
81 | try {
82 | const res = redis
83 | ? await cache.fetch(
84 | redis as Redis,
85 | `${redisPrefix}genre;${page};${genre}`,
86 | async () =>
87 | await gogoanime
88 | .fetchGenreInfo(genre, page)
89 | .catch((err) => reply.status(404).send({ message: err })),
90 | redisCacheTime,
91 | )
92 | : await gogoanime
93 | .fetchGenreInfo(genre, page)
94 | .catch((err) => reply.status(404).send({ message: err }));
95 | reply.status(200).send(res);
96 | } catch {
97 | reply
98 | .status(500)
99 | .send({ message: 'Something went wrong. Please try again later.' });
100 | }
101 | });
102 |
103 | fastify.get('/genre/list', async (request: FastifyRequest, reply: FastifyReply) => {
104 | try {
105 | const res = redis
106 | ? await cache.fetch(
107 | redis as Redis,
108 | `${redisPrefix}genre-list`,
109 | async () =>
110 | await gogoanime
111 | .fetchGenreList()
112 | .catch((err) => reply.status(404).send({ message: err })),
113 | redisCacheTime * 24,
114 | )
115 | : await gogoanime
116 | .fetchGenreList()
117 | .catch((err) => reply.status(404).send({ message: err }));
118 | reply.status(200).send(res);
119 | } catch {
120 | reply
121 | .status(500)
122 | .send({ message: 'Something went wrong. Please try again later.' });
123 | }
124 | });
125 |
126 | fastify.get(
127 | '/watch/:episodeId',
128 | async (request: FastifyRequest, reply: FastifyReply) => {
129 | const episodeId = (request.params as { episodeId: string }).episodeId;
130 | const server = (request.query as { server: StreamingServers }).server;
131 |
132 | if (server && !Object.values(StreamingServers).includes(server)) {
133 | reply.status(400).send('Invalid server');
134 | }
135 |
136 | try {
137 | const res = redis
138 | ? await cache.fetch(
139 | redis as Redis,
140 | `${redisPrefix}watch;${server};${episodeId}`,
141 | async () =>
142 | await gogoanime
143 | .fetchEpisodeSources(episodeId, server)
144 | .catch((err) => reply.status(404).send({ message: err })),
145 | redisCacheTime,
146 | )
147 | : await gogoanime
148 | .fetchEpisodeSources(episodeId, server)
149 | .catch((err) => reply.status(404).send({ message: err }));
150 |
151 | reply.status(200).send(res);
152 | } catch (err) {
153 | reply
154 | .status(500)
155 | .send({ message: 'Something went wrong. Please try again later.' });
156 | }
157 | },
158 | );
159 |
160 | fastify.get(
161 | '/servers/:episodeId',
162 | async (request: FastifyRequest, reply: FastifyReply) => {
163 | const episodeId = (request.params as { episodeId: string }).episodeId;
164 |
165 | try {
166 | const res = redis
167 | ? await cache.fetch(
168 | redis as Redis,
169 | `${redisPrefix}servers;${episodeId}`,
170 | async () =>
171 | await gogoanime
172 | .fetchEpisodeServers(episodeId)
173 | .catch((err) => reply.status(404).send({ message: err })),
174 | redisCacheTime,
175 | )
176 | : await gogoanime
177 | .fetchEpisodeServers(episodeId)
178 | .catch((err) => reply.status(404).send({ message: err }));
179 |
180 | reply.status(200).send(res);
181 | } catch (err) {
182 | reply
183 | .status(500)
184 | .send({ message: 'Something went wrong. Please try again later.' });
185 | }
186 | },
187 | );
188 |
189 | fastify.get('/top-airing', async (request: FastifyRequest, reply: FastifyReply) => {
190 | try {
191 | const page = (request.query as { page: number }).page ?? 1;
192 |
193 | const res = redis
194 | ? await cache.fetch(
195 | redis as Redis,
196 | `${redisPrefix}top-airing;${page}`,
197 | async () => await gogoanime.fetchTopAiring(page),
198 | redisCacheTime,
199 | )
200 | : await gogoanime.fetchTopAiring(page);
201 |
202 | reply.status(200).send(res);
203 | } catch (err) {
204 | reply
205 | .status(500)
206 | .send({ message: 'Something went wrong. Contact developers for help.' });
207 | }
208 | });
209 |
210 | fastify.get('/movies', async (request: FastifyRequest, reply: FastifyReply) => {
211 | try {
212 | const page = (request.query as { page: number }).page ?? 1;
213 |
214 | const res = redis
215 | ? await cache.fetch(
216 | redis as Redis,
217 | `${redisPrefix}movies;${page}`,
218 | async () => await gogoanime.fetchRecentMovies(page),
219 | redisCacheTime,
220 | )
221 | : await gogoanime.fetchRecentMovies(page);
222 |
223 | reply.status(200).send(res);
224 | } catch (err) {
225 | reply
226 | .status(500)
227 | .send({ message: 'Something went wrong. Contact developers for help.' });
228 | }
229 | });
230 |
231 | fastify.get('/popular', async (request: FastifyRequest, reply: FastifyReply) => {
232 | try {
233 | const page = (request.query as { page: number }).page ?? 1;
234 |
235 | const res = redis
236 | ? await cache.fetch(
237 | redis as Redis,
238 | `${redisPrefix}popular;${page}`,
239 | async () => await gogoanime.fetchPopular(page),
240 | redisCacheTime,
241 | )
242 | : await gogoanime.fetchPopular(page);
243 |
244 | reply.status(200).send(res);
245 | } catch (err) {
246 | reply
247 | .status(500)
248 | .send({ message: 'Something went wrong. Contact developers for help.' });
249 | }
250 | });
251 |
252 | fastify.get(
253 | '/recent-episodes',
254 | async (request: FastifyRequest, reply: FastifyReply) => {
255 | try {
256 | const type = (request.query as { type: number }).type ?? 1;
257 | const page = (request.query as { page: number }).page ?? 1;
258 |
259 | const res = redis
260 | ? await cache.fetch(
261 | redis as Redis,
262 | `${redisPrefix}recent-episodes;${page};${type}`,
263 | async () => await gogoanime.fetchRecentEpisodes(page, type),
264 | redisCacheTime,
265 | )
266 | : await gogoanime.fetchRecentEpisodes(page, type);
267 |
268 | reply.status(200).send(res);
269 | } catch (err) {
270 | reply
271 | .status(500)
272 | .send({ message: 'Something went wrong. Contact developers for help.' });
273 | }
274 | },
275 | );
276 | fastify.get('/anime-list', async (request: FastifyRequest, reply: FastifyReply) => {
277 | try {
278 | const page = (request.query as { page: number }).page ?? 1;
279 |
280 | const res = redis
281 | ? await cache.fetch(
282 | redis as Redis,
283 | `gogoanime:anime-list;${page}`,
284 | async () => await gogoanime.fetchAnimeList(page),
285 | redisCacheTime,
286 | )
287 | : await gogoanime.fetchAnimeList(page);
288 |
289 | reply.status(200).send(res);
290 | } catch (err) {
291 | reply
292 | .status(500)
293 | .send({ message: 'Something went wrong. Contact developers for help.' });
294 | }
295 | });
296 | fastify.get('/download', async (request: FastifyRequest, reply: FastifyReply) => {
297 | try {
298 | const downloadLink = (request.query as { link: string }).link;
299 | if(!downloadLink){
300 | reply.status(400).send('Invalid link');
301 | }
302 | const res = redis ? await cache.fetch(
303 | redis as Redis,
304 | `${redisPrefix}download-${downloadLink}`,
305 | async () => await gogoanime
306 | .fetchDirectDownloadLink(downloadLink)
307 | .catch((err) => reply.status(404).send({ message: err })),
308 | redisCacheTime * 24,
309 | ) : await gogoanime
310 | .fetchDirectDownloadLink(downloadLink, process.env.RECAPTCHATOKEN ?? '')
311 | .catch((err) => reply.status(404).send({ message: err }));
312 | reply.status(200).send(res);
313 | } catch {
314 | reply
315 | .status(500)
316 | .send({ message: 'Something went wrong. Please try again later.' });
317 | }
318 | });
319 | };
320 |
321 | export default routes;
322 |
--------------------------------------------------------------------------------
/routes/index.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance, RegisterOptions } from 'fastify';
2 |
3 | import gogoanime from './gogoanime';
4 |
5 | const routes = async (fastify: FastifyInstance) => {
6 | await fastify.register(gogoanime);
7 | };
8 |
9 | export default routes;
10 |
--------------------------------------------------------------------------------
/scrapper.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GogoCDN,
3 | StreamSB,
4 | } from './extractors';
5 | import {
6 | IProviderStats,
7 | ISearch,
8 | IAnimeEpisode,
9 | IAnimeInfo,
10 | IAnimeResult,
11 | IEpisodeServer,
12 | IVideo,
13 | StreamingServers,
14 | MediaStatus,
15 | SubOrSub,
16 | IMangaChapter,
17 | IMangaChapterPage,
18 | ISource,
19 | ISubtitle,
20 | Intro,
21 | Genres,
22 | FuzzyDate,
23 | ITitle,
24 | MediaFormat,
25 | ProxyConfig,
26 | } from './models';
27 |
28 | export {
29 | Genres,
30 | SubOrSub,
31 | StreamingServers,
32 | MediaStatus,
33 | IProviderStats,
34 | IAnimeEpisode,
35 | IAnimeInfo,
36 | IAnimeResult,
37 | IEpisodeServer,
38 | IVideo,
39 | IMangaChapter,
40 | ISearch,
41 | IMangaChapterPage,
42 | ISource,
43 | ISubtitle,
44 | Intro,
45 | FuzzyDate,
46 | ITitle,
47 | MediaFormat,
48 | ProxyConfig,
49 | GogoCDN,
50 | StreamSB,
51 | };
52 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2016",
4 | "module": "commonjs",
5 | "paths": {
6 | "*": ["./node_modules/*", "./types/*"]
7 | },
8 |
9 | "outDir": "dist",
10 | "esModuleInterop": true,
11 | "forceConsistentCasingInFileNames": true ,
12 | "strict": true,
13 | "skipLibCheck": true,
14 |
15 | },
16 | "include": [
17 | "**/*.ts", "main.ts", "scrapper.ts",
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "/**",
22 | "bin/**"
23 | ]
24 | }
--------------------------------------------------------------------------------
/utils/cache.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from 'ioredis';
2 | /* eslint-disable import/no-anonymous-default-export */
3 |
4 | /*
5 | TLDR; " Expires " is seconds based. for example 60*60 would = 3600 (an hour)
6 | */
7 |
8 | const fetch = async (redis: Redis, key: string, fetcher: () => T, expires: number) => {
9 | const existing = await get(redis, key);
10 | if (existing !== null) return existing;
11 |
12 | return set(redis, key, fetcher, expires);
13 | };
14 |
15 | const get = async (redis: Redis, key: string): Promise => {
16 | console.log('GET: ' + key);
17 | const value = await redis.get(key);
18 | if (value === null) return null as any;
19 |
20 | return JSON.parse(value);
21 | };
22 |
23 | const set = async (redis: Redis, key: string, fetcher: () => T, expires: number) => {
24 | console.log(`SET: ${key}, EXP: ${expires}`);
25 | const value = await fetcher();
26 | await redis.set(key, JSON.stringify(value), 'EX', expires);
27 | return value;
28 | };
29 |
30 | const del = async (redis: Redis, key: string) => {
31 | await redis.del(key);
32 | };
33 |
34 | export default { fetch, set, get, del };
35 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "/main.ts",
6 | "use": "@vercel/node"
7 | }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/(.*)",
12 | "dest": "/main.ts"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------