diff --git a/anify-backend/package.json b/anify-backend/package.json index 4b1034a..7aaf5f4 100644 --- a/anify-backend/package.json +++ b/anify-backend/package.json @@ -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", @@ -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": { diff --git a/anify-backend/src/env.ts b/anify-backend/src/env.ts index 2f120ef..22318fd 100644 --- a/anify-backend/src/env.ts +++ b/anify-backend/src/env.ts @@ -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(); @@ -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 diff --git a/anify-backend/src/mappings/impl/anime/impl/animekai.ts b/anify-backend/src/mappings/impl/anime/impl/animekai.ts new file mode 100644 index 0000000..402dbc8 --- /dev/null +++ b/anify-backend/src/mappings/impl/anime/impl/animekai.ts @@ -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 | 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 { + const response = await this.request(`${this.url}/browser?keyword=${query}`); + const $ = load(await response.text()); + + const results: IProviderResult[] = []; + const promises: Promise[] = []; + + $(".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 { + 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 { + 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; + } +} \ No newline at end of file diff --git a/anify-backend/src/mappings/impl/information/impl/tmdb.ts b/anify-backend/src/mappings/impl/information/impl/tmdb.ts index b65f7b6..54d7377 100644 --- a/anify-backend/src/mappings/impl/information/impl/tmdb.ts +++ b/anify-backend/src/mappings/impl/information/impl/tmdb.ts @@ -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 { override id = "tmdb"; diff --git a/anify-backend/src/mappings/impl/information/impl/tvdb.ts b/anify-backend/src/mappings/impl/information/impl/tvdb.ts index 09f6e4f..3ca20ad 100644 --- a/anify-backend/src/mappings/impl/information/impl/tvdb.ts +++ b/anify-backend/src/mappings/impl/information/impl/tvdb.ts @@ -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 { override id = "tvdb"; diff --git a/anify-backend/src/mappings/impl/meta/impl/tvdb.ts b/anify-backend/src/mappings/impl/meta/impl/tvdb.ts index f4fff4d..686f674 100644 --- a/anify-backend/src/mappings/impl/meta/impl/tvdb.ts +++ b/anify-backend/src/mappings/impl/meta/impl/tvdb.ts @@ -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"; diff --git a/anify-backend/src/mappings/index.ts b/anify-backend/src/mappings/index.ts index 94c6356..222c37d 100644 --- a/anify-backend/src/mappings/index.ts +++ b/anify-backend/src/mappings/index.ts @@ -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"); diff --git a/anify-backend/src/proxies/impl/manager/impl/scrape/impl/speedX.ts b/anify-backend/src/proxies/impl/manager/impl/scrape/impl/speedX.ts index 7481a8b..44c7591 100644 --- a/anify-backend/src/proxies/impl/manager/impl/scrape/impl/speedX.ts +++ b/anify-backend/src/proxies/impl/manager/impl/scrape/impl/speedX.ts @@ -4,7 +4,6 @@ const scrape = async (): Promise => { 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 { @@ -26,19 +25,6 @@ const scrape = async (): Promise => { 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; }; diff --git a/anify-backend/src/proxies/impl/request/customRequest.ts b/anify-backend/src/proxies/impl/request/customRequest.ts index 658a6fb..52d510e 100644 --- a/anify-backend/src/proxies/impl/request/customRequest.ts +++ b/anify-backend/src/proxies/impl/request/customRequest.ts @@ -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 { const { isChecking, proxy, useGoogleTranslate, timeout, providerType, providerId, maxRetries, validateResponse } = options; @@ -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, { diff --git a/anify-backend/src/types/impl/extractors/impl/baseNovelExtractor.ts b/anify-backend/src/types/impl/extractors/impl/baseNovelExtractor.ts index 9c2d78a..29ca501 100644 --- a/anify-backend/src/types/impl/extractors/impl/baseNovelExtractor.ts +++ b/anify-backend/src/types/impl/extractors/impl/baseNovelExtractor.ts @@ -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; diff --git a/anify-backend/src/types/impl/mappings/impl/anime.ts b/anify-backend/src/types/impl/mappings/impl/anime.ts index 17565de..985c3c4 100644 --- a/anify-backend/src/types/impl/mappings/impl/anime.ts +++ b/anify-backend/src/types/impl/mappings/impl/anime.ts @@ -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", } diff --git a/anify-backend/src/types/impl/mappings/impl/mediaProvider.ts b/anify-backend/src/types/impl/mappings/impl/mediaProvider.ts index 2b71cdb..ed63f7c 100644 --- a/anify-backend/src/types/impl/mappings/impl/mediaProvider.ts +++ b/anify-backend/src/types/impl/mappings/impl/mediaProvider.ts @@ -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 = new Map(); diff --git a/anify-backend/src/types/impl/proxies/index.ts b/anify-backend/src/types/impl/proxies/index.ts index 6b8a2f6..4ce6f8b 100644 --- a/anify-backend/src/types/impl/proxies/index.ts +++ b/anify-backend/src/types/impl/proxies/index.ts @@ -3,6 +3,7 @@ */ import { ProviderType } from "../.."; +import type { RequestInit, Response } from "node-fetch"; /** * @description Type of proxy diff --git a/anify-backend/src/video-extractors/impl/animekai-decoder.ts b/anify-backend/src/video-extractors/impl/animekai-decoder.ts new file mode 100644 index 0000000..3e6f76e --- /dev/null +++ b/anify-backend/src/video-extractors/impl/animekai-decoder.ts @@ -0,0 +1,273 @@ +/** + * @author Toasty360 & Dungeon69 + * @description Animekai Decoder. This is the modified and type-safe version of the original decoder which is by Dungeon69. + * @link https://github.com/Toasty360/wasm/tree/main/animekai.to + * @link https://github.com/Dungeon69/Junk/blob/master/Completed/animekai.to/decoder.js + */ + +let wasm: unknown; + + +const cachedTextDecoder = new TextDecoder("utf-8", { + ignoreBOM: true, + fatal: true, +}); + +cachedTextDecoder.decode(); + +let cachedUint8ArrayMemory0: Uint8Array | null = null; + +function getUint8ArrayMemory0(): Uint8Array { + if ( + cachedUint8ArrayMemory0 === null || + cachedUint8ArrayMemory0.byteLength === 0 + ) { + cachedUint8ArrayMemory0 = new Uint8Array((wasm as { memory: { buffer: ArrayBuffer } }).memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr: number, len: number): string { + const adjustedPtr = ptr >>> 0; + return cachedTextDecoder.decode( + getUint8ArrayMemory0().subarray(adjustedPtr, adjustedPtr + len) + ); +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = new TextEncoder(); + +const encodeString = ( + typeof cachedTextEncoder.encodeInto === "function" + ? (arg: string, view: Uint8Array) => { + return cachedTextEncoder.encodeInto(arg, view); + } + : (arg: string, view: Uint8Array) => { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length, + }; + } +); + +function passStringToWasm0( + arg: string, + malloc: (size: number, align: number) => number, + realloc?: (ptr: number, oldSize: number, newSize: number, align: number) => number +): number { + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0() + .subarray(ptr, ptr + buf.length) + .set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + const initialLen = arg.length; + let ptr = malloc(initialLen, 1) >>> 0; + let currentArg = arg; + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < initialLen; offset++) { + const code = currentArg.charCodeAt(offset); + if (code > 0x7f) break; + mem[ptr + offset] = code; + } + + if (offset !== initialLen) { + if (offset !== 0) { + currentArg = currentArg.slice(offset); + } + const newLen = offset + currentArg.length * 3; + ptr = realloc(ptr, initialLen, newLen, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + newLen); + const ret = encodeString(currentArg, view); + + offset += ret.written; + ptr = realloc(ptr, newLen, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} + +const AnimekaiDecoderFinalization = + typeof FinalizationRegistry === "undefined" + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry((ptr: number) => + (wasm as { + __wbg_animekaidecoder_free: (ptr: number, size: number) => void + }).__wbg_animekaidecoder_free(ptr >>> 0, 1) + ); + +class AnimekaiDecoder { + private __wbg_ptr = 0; + + __destroy_into_raw(): number { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + AnimekaiDecoderFinalization.unregister(this); + return ptr; + } + + free(): void { + const ptr = this.__destroy_into_raw(); + (wasm as { + __wbg_animekaidecoder_free: (ptr: number, size: number) => void + }).__wbg_animekaidecoder_free(ptr, 0); + } + + + static generate_token(n: string): string { + let deferred2_0 = 0; + let deferred2_1 = 0; + try { + const ptr0 = passStringToWasm0( + n, + (wasm as { + __wbindgen_malloc: (size: number, align: number) => number + }).__wbindgen_malloc, + (wasm as { + __wbindgen_realloc: (ptr: number, oldSize: number, newSize: number, align: number) => number + }).__wbindgen_realloc + + ); + const len0 = WASM_VECTOR_LEN; + const ret = (wasm as { + animekaidecoder_generate_token: (ptr: number, len: number) => [number, number] + }).animekaidecoder_generate_token(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + + } finally { + (wasm as { + __wbindgen_free: (ptr: number, size: number, align: number) => void + }).__wbindgen_free(deferred2_0, deferred2_1, 1); + } + } + + + static decode_iframe_data(n: string): string { + let deferred2_0 = 0; + let deferred2_1 = 0; + try { + const ptr0 = passStringToWasm0( + n, + (wasm as { + __wbindgen_malloc: (size: number, align: number) => number + }).__wbindgen_malloc, + (wasm as { + __wbindgen_realloc: (ptr: number, oldSize: number, newSize: number, align: number) => number + }).__wbindgen_realloc + ); + const len0 = WASM_VECTOR_LEN; + + const ret = (wasm as { + animekaidecoder_decode_iframe_data: (ptr: number, len: number) => [number, number] + }).animekaidecoder_decode_iframe_data(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + + } finally { + (wasm as { + __wbindgen_free: (ptr: number, size: number, align: number) => void + }).__wbindgen_free(deferred2_0, deferred2_1, 1); + } + } + + + static decode(n: string): string { + let deferred2_0 = 0; + let deferred2_1 = 0; + try { + const ptr0 = passStringToWasm0( + n, + (wasm as { + __wbindgen_malloc: (size: number, align: number) => number + }).__wbindgen_malloc, + (wasm as { + __wbindgen_realloc: (ptr: number, oldSize: number, newSize: number, align: number) => number + }).__wbindgen_realloc + ); + const len0 = WASM_VECTOR_LEN; + + const ret = (wasm as { + animekaidecoder_decode: (ptr: number, len: number) => [number, number] + }).animekaidecoder_decode(ptr0, len0); + deferred2_0 = ret[0]; + deferred2_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + + } finally { + (wasm as { + __wbindgen_free: (ptr: number, size: number, align: number) => void + }).__wbindgen_free(deferred2_0, deferred2_1, 1); + } + } +} + + +function getImports() { + return { + __wbindgen_placeholder__: { + __wbindgen_init_externref_table: () => { + const table = (wasm as { + __wbindgen_export_0: { + grow: (size: number) => number; + set: (index: number, value: unknown) => void; + }; + }).__wbindgen_export_0; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + + }, + __wbindgen_throw: (arg0: number, arg1: number) => { + throw new Error(getStringFromWasm0(arg0, arg1)); + }, + }, + }; +} + +async function initWasm(wasmModule: WebAssembly.Module | ArrayBuffer): Promise { + const imports = getImports(); + + const module = wasmModule instanceof WebAssembly.Module + ? wasmModule + : new WebAssembly.Module(wasmModule); + + const instance = new WebAssembly.Instance(module, imports); + wasm = instance.exports; + + if (typeof (wasm as { + __wbindgen_start: () => void + }).__wbindgen_start === "function") { + (wasm as { + __wbindgen_start: () => void + }).__wbindgen_start(); + } + + + + return wasm as WebAssembly.Instance; +} + +const wasmResponse = await fetch( + "https://github.com/Toasty360/wasm/raw/refs/heads/main/animekai.to/animekai_bg.wasm" +); +const arrayBuffer = await wasmResponse.arrayBuffer(); +initWasm(arrayBuffer); + +export default AnimekaiDecoder; \ No newline at end of file