Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions anify-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"prettier": "bunx prettier --write .",
"lint": "bun run prettier && bunx tsc --noEmit && bunx eslint --fix .",
"scripts": "bun src/scripts/index.ts",
"test": "bun src/tests/index.ts",
"train": " bun run src/scripts/proxyTrainer.ts --types=BASE,ANIME,MANGA,META,INFORMATION"
"test": "bun src/tests/index.ts"
},
"dependencies": {
"@extractus/article-extractor": "^8.0.16",
Expand All @@ -24,13 +23,15 @@
"epub-gen-memory": "^1.1.2",
"eventemitter2": "latest",
"fastest-levenshtein": "^1.0.16",
"https-proxy-agent": "^7.0.6",
"ioredis": "^5.4.1",
"jimp": "^0.22.12",
"lru-cache": "^11.0.2",
"node-fetch": "^3.3.2",
"p-limit": "^6.2.0",
"pdfkit": "^0.13.0",
"pg": "^8.13.1",
"undici": "^7.2.0",
"undici": "^7.3.0",
"zod": "^3.23.8"
},
"devDependencies": {
Expand Down
5 changes: 4 additions & 1 deletion anify-backend/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as dotenv from "dotenv";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "bun";

const booleanFromEnv = z.string().transform((val) => {
const normalized = val.toLowerCase().trim();
Expand Down Expand Up @@ -75,10 +76,12 @@ const envSchema = z.object({
NOVELUPDATES_LOGIN: z.string().optional(),
});

const __dirname = path.dirname(fileURLToPath(import.meta.url));

/**
* @description Set the file path for the `.env` file.
*/
const ENV_FILE_PATH = path.resolve(process.cwd(), ".env");
const ENV_FILE_PATH = path.resolve(__dirname, "..", ".env");

/**
* @description Load and parse the `.env` file
Expand Down
182 changes: 182 additions & 0 deletions anify-backend/src/mappings/impl/anime/impl/animekai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { load } from "cheerio";
import AnimeProvider from "..";
import { type ISource, StreamingServers, SubType } from "../../../../types/impl/mappings/impl/anime";
import {type IEpisode, type IProviderResult, MediaFormat } from "../../../../types";
import AnimekaiDecoder from "../../../../video-extractors/impl/animekai-decoder";

const decoder = AnimekaiDecoder

export default class AnimeKai extends AnimeProvider {
override rateLimit = 0;
override maxConcurrentRequests = -1;

override id = "animekai";
override url = "https://animekai.to";


public needsProxy = false;
public useGoogleTranslate = false;

override formats: MediaFormat[] = [MediaFormat.MOVIE, MediaFormat.ONA, MediaFormat.OVA, MediaFormat.SPECIAL, MediaFormat.TV, MediaFormat.TV_SHORT];

override get subTypes(): SubType[] {
return [SubType.SUB, SubType.DUB];
}

override get headers(): Record<string, string> | undefined {
return {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
};
}

override async search(query: string): Promise<IProviderResult[] | undefined> {
const response = await this.request(`${this.url}/browser?keyword=${query}`);
const $ = load(await response.text());

const results: IProviderResult[] = [];
const promises: Promise<void>[] = [];

$(".aitem-wrapper .aitem").each((i, el) => {
const promise = (async () => {
const dataId = $(el).find("button.ttip-btn").attr("data-tip");

if (!dataId) return;

const img = $(el).find("img").attr("data-src");
const res = await this.request(`${this.url}/ajax/anime/tip?id=${dataId}`);
const $$ = load((await res.json() as { result: string }).result);

const title = $$('.title').text().trim();
const altTitles = $$(".al-title").text().trim().split("; ").map(t => t.trim());
const id = $$("a.watch-btn").attr("href");
const airedText = $$('div > span:contains("Aired:")').parent().text();
const yearMatch = airedText.match(/\b(\d{4})\b/);
const year = yearMatch ? Number.parseInt(yearMatch[1]) : 0;

if (id) {
results.push({
id: id,
title: title,
altTitles: altTitles,
year: year,
format: MediaFormat.UNKNOWN,
img: img || '',
providerId: this.id,
});
}
})();

promises.push(promise);
});

await Promise.all(promises);
return results;
}

override async fetchEpisodes(id: string): Promise<IEpisode[] | undefined> {
const response = await this.request(`${this.url}${id.includes("/watch/") ? `${id}` : `/watch/${id}`}`);
const data = await response.text();

const dataId = data.match(/class="rate-box".*?data-id\s*=\s*["'](.*?)['"]/)?.[1];

const episodeResponse = await this.request(`${this.url}/ajax/episodes/list?ani_id=${dataId}&_=${decoder.generate_token(dataId as string)}`)

const episodeData = await episodeResponse.json() as { result: string };

const $ = load(episodeData.result);

const episodes = $("a")
.map((i, el) => {
return {
number: Number(el.attribs.num),
title: $(el).find("span").text(),
id: el.attribs.token,
isFiller: false,
img: "",
hasDub: false,

description: "",
rating: 0,
};
})
.get() as IEpisode[];

return episodes;
}

override async fetchSources(episodeId: string, subType: SubType, server: StreamingServers = StreamingServers.UpCloud): Promise<ISource | undefined> {
const linksResponse = await this.request(`${this.url}/ajax/links/list?token=${episodeId}&_=${decoder.generate_token(episodeId)}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
}
});

const linksData = await linksResponse.json() as { result: string };
const $ = load(linksData.result);

const serverGroups = $(".server-items")
// @ts-expect-error: This is a valid type
.map((_index, element) => {
const serverType = element.attribs["data-id"];
const serverList = $(element)
.find("span")
.map((i, serverElement) => ({
name: $(serverElement).text(),
id: serverElement.attribs["data-lid"],
}))
.get();

return {
[serverType]: serverList
}
})
.get() as { sub: { name: string; id: string }[]; dub: { name: string; id: string }[] }[];

const targetType = subType === SubType.SUB ? "sub" : "dub";
const targetServerName = server === StreamingServers.AnimeKaiMegacloud ? "Server 2" : "Server 1";

const typeServers = Array.isArray(serverGroups) ? serverGroups.flatMap(group => group[targetType] || []) : [];
// Normalize the server name to the expected format
const targetServer = typeServers.find((server: { name: string; id: string }) => (server.name.toLowerCase() === "megacloud" ? "server 2" : "server 1") === targetServerName.toLowerCase());
const serverId = targetServer?.id;

const sourceResponse = await this.request(`${this.url}/ajax/links/view?id=${serverId}&_=${decoder.generate_token(serverId as string)}`, {
headers: {
"X-Requested-With": "XMLHttpRequest",
}
});

const sourceResponseData = await sourceResponse.json() as { result: string };

const decodedData = JSON.parse(decoder.decode_iframe_data(sourceResponseData.result).replace(/\\/gm, "")) as { url: string };

const mediaUrl = decodedData.url.replace(/\/(e|e2)\//, "/media/");

const mediaResponse = await this.request(mediaUrl);
const mediaData = await mediaResponse.json() as { result: string };

const decodedMedia = decoder.decode(mediaData.result.replace(/\\/gm, ""));
const parsedMedia = JSON.parse(decodedMedia);

return {
sources: parsedMedia.sources.map((source: { file: string }) => ({
url: source.file,
quality: "default"
})),
subtitles: parsedMedia.tracks.map((track: { file: string; kind: string }) => ({
url: track.file,
lang: track.kind,
label: track.kind
})),
intro: {
start: 0,
end: 0
},
outro: {
start: 0,
end: 0
},
headers: {}
} as ISource;
}
}
1 change: 1 addition & 0 deletions anify-backend/src/mappings/impl/information/impl/tmdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { type IChapter, type IEpisode, MediaFormat, MediaSeason, MediaType, Prov
import type { IAnime } from "../../../../types/impl/database/impl/schema/anime";
import type { IManga } from "../../../../types/impl/database/impl/schema/manga";
import type { AnimeInfo, MangaInfo, MediaInfoKeys } from "../../../../types/impl/mappings/impl/mediaInfo";
import type { Response } from "node-fetch";

export default class TMDBInfo extends InformationProvider<IAnime | IManga, AnimeInfo | MangaInfo> {
override id = "tmdb";
Expand Down
1 change: 1 addition & 0 deletions anify-backend/src/mappings/impl/information/impl/tvdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ICharacter } from "../../../../types/impl/database/impl/mappings";
import type { IAnime } from "../../../../types/impl/database/impl/schema/anime";
import type { IManga } from "../../../../types/impl/database/impl/schema/manga";
import type { AnimeInfo, MangaInfo, MediaInfoKeys } from "../../../../types/impl/mappings/impl/mediaInfo";
import type { Response } from "node-fetch";

export default class TVDBInfo extends InformationProvider<IAnime | IManga, AnimeInfo | MangaInfo> {
override id = "tvdb";
Expand Down
1 change: 1 addition & 0 deletions anify-backend/src/mappings/impl/meta/impl/tvdb.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import MetaProvider from "..";
import { IProviderResult, MediaFormat } from "../../../../types";
import type { Response } from "node-fetch";

export default class TVDBMeta extends MetaProvider {
override id = "tvdb";
Expand Down
5 changes: 5 additions & 0 deletions anify-backend/src/mappings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@ export const ANIME_PROVIDERS = [
const { default: HiAnime } = await import("./impl/anime/impl/hianime");
return new HiAnime();
},
async () => {
const { default: AnimeKai } = await import("./impl/anime/impl/animekai");
return new AnimeKai();
},
];


export const MANGA_PROVIDERS = [
async () => {
const { default: MangaDex } = await import("./impl/manga/impl/mangadex");
Expand Down
14 changes: 0 additions & 14 deletions anify-backend/src/proxies/impl/manager/impl/scrape/impl/speedX.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const scrape = async (): Promise<IProxy[]> => {
const http = await (await fetch("https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/http.txt")).text();
const socks5 = await (await fetch("https://raw.githubusercontent.com/TheSpeedX/SOCKS-List/master/socks5.txt")).text();

/*
const proxyList = http.split("\n").map((line) => {
const [host, port] = line.split(":");
return {
Expand All @@ -26,19 +25,6 @@ const scrape = async (): Promise<IProxy[]> => {
providerMetrics: {},
} as IProxy;
}));
*/

const proxyList = socks5.split("\n").map((line) => {
const [host, port] = line.split(":");
return {
port: parseInt(port),
anonymity: "unknown",
country: "unknown",
ip: host,
type: ProxyType.SOCKS5,
providerMetrics: {},
} as IProxy;
});

return proxyList;
};
Expand Down
3 changes: 2 additions & 1 deletion anify-backend/src/proxies/impl/request/customRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IRequestConfig } from "../../../types/impl/proxies";
import { ProxyAgent } from "undici";
import { SocksProxyAgent } from "socks-proxy-agent";
import { updateProxyHealth, proxyCache, selectProxy, proxyToUrl } from "../manager";
import fetch, { type RequestInit, type Response } from "node-fetch";

export async function customRequest(url: string, options: IRequestConfig = {}): Promise<Response> {
const { isChecking, proxy, useGoogleTranslate, timeout, providerType, providerId, maxRetries, validateResponse } = options;
Expand All @@ -27,7 +28,7 @@ export async function customRequest(url: string, options: IRequestConfig = {}):
// Determine if it's a SOCKS5 or HTTP proxy based on the URL scheme
if (proxyURL.startsWith("socks5://")) {
Object.assign(fetchOptions, {
dispatcher: new SocksProxyAgent(proxyURL),
agent: new SocksProxyAgent(proxyURL),
});
} else {
Object.assign(fetchOptions, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { selectProxy, proxyToUrl } from "../../../../proxies/impl/manager";
import { customRequest } from "../../../../proxies/impl/request/customRequest";
import type { IPage, NovelProviders } from "../../mappings/impl/manga";
import type { IRequestConfig } from "../../proxies";
import type { Response } from "node-fetch";

export default abstract class BaseNovelExtractor implements INovelExtractor {
abstract url: string;
Expand Down
7 changes: 5 additions & 2 deletions anify-backend/src/types/impl/mappings/impl/anime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,21 @@ export type IServer = {
/**
* @description Enum for the sub type of the anime.
*/
export const enum SubType {
export enum SubType {
DUB = "dub",
SUB = "sub",
}


/**
* @description Enum for streaming servers that can be extracted.
*/
export const enum StreamingServers {
export enum StreamingServers {
GogoCDN = "gogocdn",
Kwik = "kwik",
VidStreaming = "vidstreaming",
StreamSB = "streamsb",
VidCloud = "vidcloud",
UpCloud = "upcloud",
AnimeKaiMegacloud = "animekai-megacloud",
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ProviderType } from "../../..";
import type { IRequestConfig } from "../../proxies";
import { selectProxy, proxyToUrl } from "../../../../proxies/impl/manager";
import { customRequest } from "../../../../proxies/impl/request/customRequest";
import type { Response } from "node-fetch";

export abstract class MediaProvider {
private static limiterMap: Map<string, Bottleneck> = new Map();
Expand Down
1 change: 1 addition & 0 deletions anify-backend/src/types/impl/proxies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

import { ProviderType } from "../..";
import type { RequestInit, Response } from "node-fetch";

/**
* @description Type of proxy
Expand Down
Loading
Loading