├── .eslintignore ├── src ├── reset.d.ts ├── modules │ ├── anify │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── hhhay │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── ophim │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── anime47 │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── animehay │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── animetvn │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── aniwatch │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── aniwave │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── anizone │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── hanime │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── hentaiz │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── sudatchi │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── animepahe │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ ├── animevietsub │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts │ └── kickassanime │ │ ├── logo.png │ │ ├── metadata.json │ │ └── index.ts ├── schemas │ ├── file-url.ts │ ├── timestamp.ts │ ├── video-server.ts │ ├── font.ts │ ├── search-result.ts │ ├── module.ts │ ├── subtitle.ts │ ├── episode.ts │ ├── video.ts │ └── video-container.ts ├── utils │ ├── index.ts │ ├── module-cli.ts │ ├── executor.ts │ └── modules.ts ├── scripts │ ├── module-build.ts │ ├── module-validate.ts │ ├── module-index.ts │ └── module-test.ts ├── module.d.ts └── types │ └── index.ts ├── .gitignore ├── .nycrc ├── .eslintrc ├── .github └── workflows │ └── build-module.yaml ├── LICENSE ├── CHANGELOG.md ├── package.json ├── tsconfig.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /src/reset.d.ts: -------------------------------------------------------------------------------- 1 | import "@total-typescript/ts-reset"; 2 | -------------------------------------------------------------------------------- /src/modules/anify/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/anify/logo.png -------------------------------------------------------------------------------- /src/modules/hhhay/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/hhhay/logo.png -------------------------------------------------------------------------------- /src/modules/ophim/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/ophim/logo.png -------------------------------------------------------------------------------- /src/modules/anime47/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/anime47/logo.png -------------------------------------------------------------------------------- /src/modules/animehay/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/animehay/logo.png -------------------------------------------------------------------------------- /src/modules/animetvn/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/animetvn/logo.png -------------------------------------------------------------------------------- /src/modules/aniwatch/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/aniwatch/logo.png -------------------------------------------------------------------------------- /src/modules/aniwave/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/aniwave/logo.png -------------------------------------------------------------------------------- /src/modules/anizone/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/anizone/logo.png -------------------------------------------------------------------------------- /src/modules/hanime/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/hanime/logo.png -------------------------------------------------------------------------------- /src/modules/hentaiz/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/hentaiz/logo.png -------------------------------------------------------------------------------- /src/modules/sudatchi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/sudatchi/logo.png -------------------------------------------------------------------------------- /src/modules/animepahe/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/animepahe/logo.png -------------------------------------------------------------------------------- /src/modules/animevietsub/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/animevietsub/logo.png -------------------------------------------------------------------------------- /src/modules/kickassanime/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoangvu12/kaguya-modules/HEAD/src/modules/kickassanime/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated directories: 2 | .nyc_output/ 3 | coverage/ 4 | dist/ 5 | node_modules/ 6 | 7 | # Project files: 8 | *.sublime-* 9 | 10 | output -------------------------------------------------------------------------------- /src/schemas/file-url.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const FileUrlSchema = z.object({ 4 | url: z.string(), 5 | headers: z.record(z.string()).nullable().optional(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/schemas/timestamp.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const TimestampSchema = z.object({ 4 | type: z.string(), 5 | startTime: z.number(), 6 | endTime: z.number(), 7 | }); 8 | -------------------------------------------------------------------------------- /src/schemas/video-server.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const VideoServerSchema = z.object({ 4 | name: z.string(), 5 | extraData: z.record(z.any()).nullable().optional(), 6 | }); 7 | -------------------------------------------------------------------------------- /src/schemas/font.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { FileUrlSchema } from "./file-url"; 4 | 5 | export const FontSchema = z.object({ 6 | file: FileUrlSchema, 7 | name: z.string(), 8 | }); 9 | -------------------------------------------------------------------------------- /src/schemas/search-result.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const SearchResultSchema = z.object({ 4 | id: z.string(), 5 | title: z.string(), 6 | thumbnail: z.string(), 7 | extra: z.record(z.any()).nullable().optional(), 8 | }); 9 | -------------------------------------------------------------------------------- /src/modules/anify/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "anify", 3 | "name": "Anify", 4 | "version": "0.0.1", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/animevietsub/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "animevietsub", 3 | "name": "AnimeVietsub", 4 | "version": "0.0.4", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/hanime/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hanime", 3 | "name": "HAnime", 4 | "version": "0.0.1", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/hhhay/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hhhay", 3 | "name": "HHHay", 4 | "version": "0.0.4", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/ophim/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ophim", 3 | "name": "OPhim", 4 | "version": "0.0.1", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/anime47/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "anime47", 3 | "name": "Anime47", 4 | "version": "0.0.6", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/aniwatch/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "aniwatch", 3 | "name": "AniWatch", 4 | "version": "0.1.2", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/aniwave/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "aniwave", 3 | "name": "AniWave", 4 | "version": "0.0.3", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/anizone/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "anizone", 3 | "name": "AniZone", 4 | "version": "0.0.2", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/hentaiz/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "hentaiz", 3 | "name": "HentaiZ", 4 | "version": "0.0.6", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/sudatchi/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "sudatchi", 3 | "name": "Sudatchi", 4 | "version": "0.0.6", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/animehay/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "animehay", 3 | "name": "AnimeHay", 4 | "version": "0.0.8", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/animepahe/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "animepahe", 3 | "name": "AnimePahe", 4 | "version": "0.0.4", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/animetvn/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "animetvn", 3 | "name": "AnimeTVN", 4 | "version": "0.0.5", 5 | "languages": [ 6 | "Tiếng Việt" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/kickassanime/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "kickassanime", 3 | "name": "KickAssAnime", 4 | "version": "0.0.1", 5 | "languages": [ 6 | "English" 7 | ], 8 | "type": "anime", 9 | "info": { 10 | "author": "hoangvu12" 11 | } 12 | } -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | 4 | export const handlePath = ( 5 | filePath: string, 6 | baseUrl: string = path.resolve(process.cwd(), "./dist") 7 | ) => path.join(baseUrl, filePath); 8 | 9 | export const hasPath = (filePath: string) => { 10 | return fs.existsSync(filePath); 11 | }; 12 | -------------------------------------------------------------------------------- /src/schemas/module.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const ModuleSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | version: z.string(), 7 | languages: z.array(z.string()), 8 | info: z.object({ 9 | author: z.string(), 10 | }), 11 | type: z.enum(["anime", "manga"]), 12 | url: z.string().url().optional(), 13 | }); 14 | 15 | export default ModuleSchema; 16 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "branches": 95, 4 | "extension": [ 5 | ".js", 6 | ".ts", 7 | ".tsx" 8 | ], 9 | "functions": 100, 10 | "include": [ 11 | "dist/**/*.js", 12 | "src/**/*.ts" 13 | ], 14 | "lines": 95, 15 | "reporter": [ 16 | "html", 17 | "text-summary" 18 | ], 19 | "require": [ 20 | "ts-node/register" 21 | ], 22 | "statements": 95 23 | } -------------------------------------------------------------------------------- /src/schemas/subtitle.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { FileUrlSchema } from "./file-url"; 4 | 5 | export enum SubtitleFormat { 6 | VTT = "vtt", 7 | ASS = "ass", 8 | SRT = "srt", 9 | } 10 | 11 | export const SubtitleFormatSchema = z.nativeEnum(SubtitleFormat); 12 | 13 | export const SubtitleSchema = z.object({ 14 | language: z.string(), 15 | file: FileUrlSchema, 16 | format: SubtitleFormatSchema.nullable().optional(), 17 | }); 18 | -------------------------------------------------------------------------------- /src/schemas/episode.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const EpisodeSchema = z.object({ 4 | number: z.string(), 5 | id: z.string(), 6 | title: z.string().nullable().optional(), 7 | isFiller: z.boolean().nullable().optional(), 8 | description: z.string().nullable().optional(), 9 | thumbnail: z.string().nullable().optional(), 10 | extra: z.record(z.any()).nullable().optional(), 11 | section: z.string().nullable().optional(), 12 | }); 13 | -------------------------------------------------------------------------------- /src/schemas/video.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { FileUrlSchema } from "./file-url"; 4 | 5 | export enum VideoFormat { 6 | CONTAINER = "container", 7 | HLS = "hls", 8 | DASH = "dash", 9 | } 10 | 11 | export const VideoFormatSchema = z.nativeEnum(VideoFormat); 12 | 13 | export const VideoSchema = z.object({ 14 | quality: z.string().nullable().optional(), 15 | format: VideoFormatSchema.optional(), 16 | file: FileUrlSchema, 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/module-cli.ts: -------------------------------------------------------------------------------- 1 | import prompts from "prompts"; 2 | import { getModuleIds } from "./modules"; 3 | 4 | export const inputModuleId = async () => { 5 | const moduleIds = await getModuleIds(); 6 | 7 | const { module_id } = await prompts({ 8 | type: "select", 9 | name: "module_id", 10 | message: "Select the module", 11 | choices: moduleIds.map((id) => ({ 12 | title: id, 13 | value: id, 14 | })), 15 | }); 16 | 17 | return module_id; 18 | }; 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "rules": { 11 | "quotes": ["off"], 12 | "@typescript-eslint/no-explicit-any": "off", 13 | "@typescript-eslint/no-non-null-assertion": "off", 14 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/schemas/video-container.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { FontSchema } from "./font"; 4 | import { SubtitleSchema } from "./subtitle"; 5 | import { TimestampSchema } from "./timestamp"; 6 | import { VideoSchema } from "./video"; 7 | 8 | export const VideoContainerSchema = z.object({ 9 | videos: z.array(VideoSchema).nonempty(), 10 | subtitles: z.array(SubtitleSchema).nullable().optional(), 11 | fonts: z.array(FontSchema).nullable().optional(), 12 | timestamps: z.array(TimestampSchema).nullable().optional(), 13 | }); 14 | -------------------------------------------------------------------------------- /src/scripts/module-build.ts: -------------------------------------------------------------------------------- 1 | import ora from "ora"; 2 | import { inputModuleId } from "../utils/module-cli"; 3 | import { archiveModule, copyLogo, rewriteIndexFile } from "../utils/modules"; 4 | 5 | const main = async () => { 6 | const module_id = await inputModuleId(); 7 | 8 | const copySpinner = ora("Copying logo to dist").start(); 9 | 10 | if (!(await copyLogo(module_id))) { 11 | copySpinner.fail("Failed to copy logo to dist"); 12 | 13 | return; 14 | } 15 | 16 | copySpinner.succeed("Logo copied to dist"); 17 | 18 | const rewriteSpinner = ora("Rewriting index file").start(); 19 | 20 | if (!(await rewriteIndexFile(module_id))) { 21 | rewriteSpinner.fail("Failed to rewrite index file"); 22 | 23 | return; 24 | } 25 | 26 | rewriteSpinner.succeed("Index file rewritten"); 27 | 28 | const spinner = ora("Building module").start(); 29 | 30 | await archiveModule(module_id, { 31 | onError(err) { 32 | spinner.fail(`Failed to build module (${err.message})`); 33 | }, 34 | onFinished() { 35 | spinner.succeed(`Module built successfully`); 36 | }, 37 | }); 38 | }; 39 | 40 | main(); 41 | -------------------------------------------------------------------------------- /.github/workflows/build-module.yaml: -------------------------------------------------------------------------------- 1 | name: Build module index 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | upload: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Check out code 13 | uses: actions/checkout@v2 14 | 15 | - name: Get repo name 16 | run: echo "REPOSITORY_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV 17 | 18 | - name: Install dependencies 19 | run: npm install 20 | 21 | - name: Build modules and index 22 | run: npm run module:index -- --index-name=${{ env.REPOSITORY_NAME }} --index-author=${{github.repository_owner}} --index-url-pattern="${{ github.server_url }}/${{ github.repository }}/raw/modules/{{module_id}}.kmodule" 23 | 24 | - name: Push to another branch 25 | uses: s0/git-publish-subdir-action@develop 26 | env: 27 | REPO: self 28 | BRANCH: modules # The branch name where you want to push the assets 29 | FOLDER: output # The directory where your assets are generated 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GitHub will automatically add this - you don't need to bother getting a token 31 | MESSAGE: "Build: ({sha}) {msg}" # The commit message 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Chris Wells (https://chriswells.io) 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/utils/executor.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | import { JSDOM } from "jsdom"; 3 | 4 | export type ExecuteCodeFunction = ( 5 | functionName: string, 6 | args?: Record 7 | ) => Promise; 8 | 9 | const sendRequest = async (config: AxiosRequestConfig) => { 10 | const response = await axios(config); 11 | 12 | return { 13 | data: response.data, 14 | status: response.status, 15 | statusText: response.statusText, 16 | headers: response.headers, 17 | }; 18 | }; 19 | 20 | export const createExecutor = () => { 21 | const dom = new JSDOM("

Hello world

", { 22 | runScripts: "dangerously", 23 | resources: "usable", 24 | }); 25 | 26 | function loadScript(url: string) { 27 | return new Promise((resolve, reject) => { 28 | if (dom.window.document.querySelector(`script[src="${url}"]`)) { 29 | resolve(""); 30 | return; 31 | } 32 | 33 | const script = dom.window.document.createElement("script"); 34 | 35 | script.src = url; 36 | script.onload = resolve; 37 | script.onerror = reject; 38 | 39 | dom.window.document.head.appendChild(script); 40 | }); 41 | } 42 | 43 | dom.window.sendRequest = sendRequest; 44 | dom.window.loadScript = loadScript; 45 | 46 | const executeCode: ExecuteCodeFunction = (functionName, args) => { 47 | return new Promise((resolve, reject) => { 48 | const timeout = setTimeout(() => { 49 | reject(new Error("Timeout")); 50 | }, 30000); 51 | 52 | dom.window.sendResponse = (data: any) => { 53 | clearTimeout(timeout); 54 | 55 | resolve(data); 56 | }; 57 | 58 | dom.window.eval(` 59 | ${functionName}(${JSON.stringify(args)}); 60 | `); 61 | }); 62 | }; 63 | 64 | const injectCode = (code: string) => { 65 | dom.window.eval("window.anime = {};"); 66 | 67 | dom.window.eval(code); 68 | }; 69 | 70 | return { dom, injectCode, executeCode }; 71 | }; 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.7](https://github.com/chriswells0/node-typescript-template/compare/v1.0.6...v1.0.7) (2023-07-08) 2 | 3 | 4 | 5 | ## [1.0.6](https://github.com/chriswells0/node-typescript-template/compare/v1.0.5...v1.0.6) (2023-07-08) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * make "clean" script command work cross-platform ([43243c4](https://github.com/chriswells0/node-typescript-template/commit/43243c4707342f332bf5819b31e7f812978fd7c5)), closes [#356](https://github.com/chriswells0/node-typescript-template/issues/356) 11 | 12 | 13 | 14 | ## [1.0.5](https://github.com/chriswells0/node-typescript-template/compare/v1.0.4...v1.0.5) (2023-03-02) 15 | 16 | 17 | 18 | ## [1.0.4](https://github.com/chriswells0/node-typescript-template/compare/v1.0.3...v1.0.4) (2020-02-23) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * Make NPM run scripts compatible with Windows. ([bbe65f8](https://github.com/chriswells0/node-typescript-template/commit/bbe65f8780568eb20dbadc9ac59b08c2b9772d88)), closes [#5](https://github.com/chriswells0/node-typescript-template/issues/5) 24 | 25 | 26 | 27 | ## [1.0.3](https://github.com/chriswells0/node-typescript-template/compare/v1.0.2...v1.0.3) (2019-04-20) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * Upgrade all dependencies to their latest versions. ([b6f2076](https://github.com/chriswells0/node-typescript-template/commit/b6f2076)), closes [#4](https://github.com/chriswells0/node-typescript-template/issues/4) 33 | 34 | 35 | 36 | ## [1.0.2](https://github.com/chriswells0/node-typescript-template/compare/v1.0.1...v1.0.2) (2019-02-23) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * Upgrade all dependencies to their latest versions. ([0212764](https://github.com/chriswells0/node-typescript-template/commit/0212764)), closes [#2](https://github.com/chriswells0/node-typescript-template/issues/2) 42 | 43 | 44 | 45 | ## [1.0.1](https://github.com/chriswells0/node-typescript-template/compare/v1.0.0...v1.0.1) (2019-02-05) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * Update dependencies and upgrade ts-node to v8. ([fee70dd](https://github.com/chriswells0/node-typescript-template/commit/fee70dd)), closes [#1](https://github.com/chriswells0/node-typescript-template/issues/1) 51 | 52 | 53 | 54 | # 1.0.0 (2018-11-10) 55 | 56 | 57 | ### Features 58 | 59 | * Add initial project template files. ([2c22c37](https://github.com/chriswells0/node-typescript-template/commit/2c22c37)) 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/scripts/module-validate.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import { hasPath } from "../utils"; 4 | import { getModuleFolder, validateMetadata } from "../utils/modules"; 5 | 6 | import ora from "ora"; 7 | import { inputModuleId } from "../utils/module-cli"; 8 | 9 | const checkIndexFile = async (moduleFolder: string) => { 10 | const file = path.join(moduleFolder, "index.js"); 11 | const spinner = ora("Is there index.js file?").start(); 12 | 13 | if (!hasPath(file)) { 14 | spinner.fail("index.js file not found"); 15 | return false; 16 | } 17 | 18 | spinner.succeed("index.js file found"); 19 | 20 | return true; 21 | }; 22 | 23 | const checkLogoFile = async (moduleFolder: string) => { 24 | const file = path.join(moduleFolder, "logo.png"); 25 | const spinner = ora("Is there logo.png file?").start(); 26 | 27 | if (!hasPath(file)) { 28 | spinner.warn("logo.png file not found, there should be a logo.png file"); 29 | } else { 30 | spinner.succeed(); 31 | } 32 | }; 33 | 34 | const checkMetadataFile = async (moduleFolder: string) => { 35 | const file = path.join(moduleFolder, "metadata.json"); 36 | const spinner = ora("Is there metadata.json file?").start(); 37 | 38 | if (!hasPath(file)) { 39 | spinner.fail("metadata.json file not found"); 40 | return false; 41 | } 42 | 43 | const metadataString = await fs.readFile(file, "utf-8"); 44 | 45 | let metadata = null; 46 | 47 | try { 48 | metadata = JSON.parse(metadataString); 49 | } catch (error) { 50 | spinner.fail("metadata.json file is not a valid JSON"); 51 | return false; 52 | } 53 | 54 | const validateResult = validateMetadata(metadata); 55 | 56 | if (!validateResult.success) { 57 | spinner.fail( 58 | `metadata.json file is not a valid metadata (${validateResult.error.message})` 59 | ); 60 | return false; 61 | } 62 | 63 | spinner.succeed("metadata.json validated"); 64 | 65 | return true; 66 | }; 67 | 68 | export const validateModule = async (module_id: string) => { 69 | const moduleFolder = await getModuleFolder(module_id); 70 | 71 | if (!checkIndexFile(moduleFolder)) { 72 | return; 73 | } 74 | 75 | checkLogoFile(moduleFolder); 76 | 77 | if (!checkMetadataFile(moduleFolder)) { 78 | return; 79 | } 80 | }; 81 | 82 | const main = async () => { 83 | const module_id = await inputModuleId(); 84 | 85 | validateModule(module_id); 86 | }; 87 | 88 | main(); 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-template", 3 | "version": "1.0.7", 4 | "description": "A complete Node.js project template using TypeScript and following general best practices.", 5 | "keywords": [ 6 | "typescript", 7 | "template" 8 | ], 9 | "author": { 10 | "name": "Chris Wells", 11 | "url": "https://chriswells.io" 12 | }, 13 | "license": "BSD-3-Clause", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/chriswells0/node-typescript-template.git" 17 | }, 18 | "main": "dist/index.js", 19 | "types": "dist/index.d.ts", 20 | "files": [ 21 | "dist/" 22 | ], 23 | "scripts": { 24 | "build": "npm run clean && npm run lint && tsc", 25 | "clean": "node -e \"var { rmdirSync, existsSync } = require('fs'), path = require('path'); ['./.nyc_output', './coverage', './dist'].forEach(fPath => {if (existsSync(path.join(__dirname, fPath))) rmdirSync(path.join(__dirname, fPath), { recursive: true })}); process.exit(0);\"", 26 | "clean:all": "npm run clean && (rm -r ./node_modules || true)", 27 | "lint": "eslint --ext .ts --fix src/**/*.ts", 28 | "preversion": "npm run build", 29 | "prepare": "npm run build", 30 | "serve": "nodemon ./src/index.ts", 31 | "upgrade": "npx npm-check -u", 32 | "watch": "tsc -w -p tsconfig.json", 33 | "module:validate": "npm run build && node ./dist/scripts/module-validate.js", 34 | "module:test": "npm run build && node ./dist/scripts/module-test.js", 35 | "module:build": "npm run build && node ./dist/scripts/module-build.js", 36 | "module:index": "npm run build && node ./dist/scripts/module-index.js" 37 | }, 38 | "devDependencies": { 39 | "@total-typescript/ts-reset": "^0.6.1", 40 | "@types/jsdom": "^21.1.6", 41 | "@types/node": "^20.4.1", 42 | "@types/prompts": "^2.4.9", 43 | "@types/web": "^0.0.161", 44 | "@typescript-eslint/eslint-plugin": "^5.61.0", 45 | "@typescript-eslint/parser": "^5.61.0", 46 | "eslint": "^8.44.0", 47 | "nodemon": "^3.0.1", 48 | "nyc": "^15.1.0", 49 | "source-map-support": "^0.5.21", 50 | "ts-node": "^10.9.1", 51 | "@types/archiver": "^6.0.2", 52 | "typescript": "^5.1.6" 53 | }, 54 | "config": { 55 | "commitizen": { 56 | "path": "./node_modules/cz-conventional-changelog" 57 | } 58 | }, 59 | "dependencies": { 60 | "archiver": "^6.0.1", 61 | "axios": "^1.6.2", 62 | "jsdom": "^22.1.0", 63 | "ora": "^5.4.1", 64 | "prompts": "^2.4.2", 65 | "zod": "^3.22.4" 66 | } 67 | } -------------------------------------------------------------------------------- /src/module.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | export enum VideoFormat { 3 | CONTAINER = "container", 4 | HLS = "hls", 5 | DASH = "dash", 6 | } 7 | 8 | export enum SubtitleFormat { 9 | VTT = "vtt", 10 | ASS = "ass", 11 | SRT = "srt", 12 | } 13 | 14 | export type FileUrl = { 15 | url: string; 16 | headers?: Record; 17 | }; 18 | 19 | export type Subtitle = { 20 | language: string; 21 | file: FileUrl; 22 | format: SubtitleFormat; 23 | }; 24 | 25 | export type Timestamp = { 26 | type: string; 27 | startTime: number; 28 | endTime: number; 29 | }; 30 | 31 | export type Video = { 32 | quality?: string; 33 | format?: VideoFormat; 34 | file: FileUrl; 35 | }; 36 | 37 | export type Episode = { 38 | number: string; 39 | id: string; 40 | title?: string; 41 | isFiller?: boolean; 42 | description?: string; 43 | thumbnail?: string; 44 | extra?: Record; 45 | section?: string; 46 | }; 47 | 48 | export type VideoServer = { 49 | name: string; 50 | extraData?: Record; 51 | }; 52 | 53 | export type VideoContainer = { 54 | videos: Video[]; 55 | subtitles: Subtitle[]; 56 | timestamps: Timestamp[]; 57 | }; 58 | 59 | export type GetIdParams = { 60 | media: { 61 | id: number; 62 | title: { 63 | romaji: string; 64 | english: string; 65 | userPreferred: string; 66 | native: string; 67 | }; 68 | }; 69 | }; 70 | 71 | export type GetEpisodeParams = { 72 | animeId: string; 73 | extraData?: Record; 74 | }; 75 | 76 | export type GetVideoServersParams = { 77 | episodeId: string; 78 | extraData?: Record; 79 | }; 80 | 81 | export type RequestConfig = { 82 | url: string; 83 | method: string; 84 | baseURL: string; 85 | headers: Record; 86 | params: Record; 87 | data: 88 | | string 89 | | Record 90 | | ArrayBuffer 91 | | ArrayBufferView 92 | | URLSearchParams; 93 | timeout: number; 94 | withCredentials: boolean; 95 | responseType: string; 96 | responseEncoding: string; 97 | xsrfCookieName: string; 98 | xsrfHeaderName: string; // default 99 | maxContentLength: number; 100 | maxBodyLength: number; 101 | maxRedirects: number; 102 | socketPath: string | null; // default 103 | decompress: boolean; // default 104 | }; 105 | 106 | declare global { 107 | function sendRequest( 108 | config: string | Partial 109 | ): Promise<{ 110 | data: T; 111 | status: number; 112 | statusText: string; 113 | headers: Record; 114 | request?: any; 115 | }>; 116 | 117 | function sendResponse(response: T): void; 118 | async function loadScript(url: string): Promise; 119 | } 120 | 121 | export {}; 122 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type FileUrl = { 2 | url: string; 3 | headers?: Record; 4 | }; 5 | 6 | export type Subtitle = { 7 | language: string; 8 | file: FileUrl; 9 | format?: "vtt" | "ass" | "srt"; 10 | }; 11 | 12 | export type Timestamp = { 13 | type: string; 14 | startTime: number; 15 | endTime: number; 16 | }; 17 | 18 | export type Video = { 19 | quality?: string; 20 | format?: "container" | "hls" | "dash"; 21 | file: FileUrl; 22 | }; 23 | 24 | export type Episode = { 25 | number: string; 26 | id: string; 27 | title?: string; 28 | isFiller?: boolean; 29 | description?: string; 30 | thumbnail?: string; 31 | extra?: Record; 32 | section?: string; 33 | }; 34 | 35 | export type VideoServer = { 36 | name: string; 37 | extraData?: Record; 38 | }; 39 | 40 | export type Font = { 41 | name: string; 42 | file: FileUrl; 43 | }; 44 | 45 | export type VideoContainer = { 46 | videos: Video[]; 47 | subtitles?: Subtitle[]; 48 | timestamps?: Timestamp[]; 49 | fonts?: Font[]; 50 | }; 51 | 52 | export type SearchResult = { 53 | id: string; 54 | title: string; 55 | thumbnail: string; 56 | extra?: Record; 57 | }; 58 | 59 | export type GetIdParams = { 60 | media: { 61 | id: number; 62 | title: { 63 | romaji: string; 64 | english: string; 65 | userPreferred: string; 66 | native: string; 67 | }; 68 | }; 69 | }; 70 | 71 | export type GetEpisodeParams = { 72 | animeId: string; 73 | extraData?: Record; 74 | }; 75 | 76 | export type GetVideoServersParams = { 77 | episodeId: string; 78 | extraData?: Record; 79 | }; 80 | 81 | export type SearchParams = { 82 | query: string; 83 | }; 84 | 85 | export type RequestConfig = { 86 | url: string; 87 | method: string; 88 | baseURL: string; 89 | headers: Record; 90 | params: Record; 91 | data: 92 | | string 93 | | Record 94 | | ArrayBuffer 95 | | ArrayBufferView 96 | | URLSearchParams; 97 | timeout: number; 98 | withCredentials: boolean; 99 | responseType: string; 100 | responseEncoding: string; 101 | xsrfCookieName: string; 102 | xsrfHeaderName: string; // default 103 | maxContentLength: number; 104 | maxBodyLength: number; 105 | maxRedirects: number; 106 | socketPath: string | null; // default 107 | decompress: boolean; // default 108 | }; 109 | 110 | export type Anime = { 111 | // Promise<{ data: string; extraData?: Record }>; 112 | getId(params: GetIdParams): Promise; 113 | 114 | // Promise; 115 | getEpisodes(params: GetEpisodeParams): Promise; 116 | 117 | // Promise; 118 | loadVideoServers(params: GetVideoServersParams): Promise; 119 | 120 | // Promise 121 | loadVideoContainer(server: VideoServer): Promise; 122 | 123 | // Promise; 124 | search(params: SearchParams): Promise; 125 | }; 126 | -------------------------------------------------------------------------------- /src/modules/anify/index.ts: -------------------------------------------------------------------------------- 1 | import type { Anime, VideoContainer } from "../../types/index"; 2 | 3 | interface WindowAnime extends Anime { 4 | baseUrl: string; 5 | } 6 | 7 | const anime: WindowAnime = { 8 | baseUrl: "https://api.anify.tv", 9 | getId: async ({ media }) => { 10 | sendResponse({ 11 | data: media.id.toString(), 12 | }); 13 | }, 14 | getEpisodes: async ({ animeId }) => { 15 | const { data } = await sendRequest(`${anime.baseUrl}/episodes/${animeId}`); 16 | 17 | if (!data?.length) { 18 | sendResponse([]); 19 | 20 | return; 21 | } 22 | 23 | const episodes = data.flatMap((provider: any) => { 24 | const providerEpisodes = provider.episodes.map((episode: any) => ({ 25 | number: episode.number.toString(), 26 | id: episode.id, 27 | title: episode.title, 28 | thumbnail: episode.img, 29 | isFiller: episode.isFiller, 30 | section: provider.providerId, 31 | extra: { 32 | ...episode, 33 | providerId: provider.providerId, 34 | mediaId: animeId, 35 | }, 36 | })); 37 | 38 | return providerEpisodes; 39 | }); 40 | 41 | sendResponse(episodes); 42 | }, 43 | search: async () => { 44 | sendResponse([]); 45 | }, 46 | async loadVideoServers({ extraData }) { 47 | sendResponse([ 48 | { 49 | name: "Default", 50 | extraData: extraData, 51 | }, 52 | ]); 53 | }, 54 | 55 | async loadVideoContainer({ extraData }) { 56 | const container: VideoContainer = { 57 | videos: [], 58 | subtitles: [], 59 | timestamps: [], 60 | }; 61 | 62 | if (!extraData?.id) { 63 | return sendResponse(container); 64 | } 65 | 66 | const { data } = await sendRequest( 67 | `${anime.baseUrl}/sources?providerId=${ 68 | extraData?.providerId 69 | }&watchId=${encodeURIComponent(extraData?.id)}&episodeNumber=${ 70 | extraData?.number 71 | }&id=${extraData?.mediaId}&subType=sub` 72 | ); 73 | 74 | if (!data?.sources?.length) throw new Error("No sources found"); 75 | 76 | container.videos = data.sources.map((source: any) => ({ 77 | quality: source.quality, 78 | file: { 79 | url: source.url, 80 | headers: data.headers || {}, 81 | }, 82 | })); 83 | 84 | if (data?.subtitles?.length) { 85 | container.subtitles = data.subtitles.map((subtitle: any) => ({ 86 | file: { 87 | url: subtitle.url, 88 | }, 89 | language: subtitle.lang, 90 | })); 91 | } 92 | 93 | if (data?.intro) { 94 | container.timestamps?.push({ 95 | type: "Intro", 96 | startTime: data.intro.start, 97 | endTime: data.intro.end, 98 | }); 99 | } 100 | 101 | if (data?.outro) { 102 | container.timestamps?.push({ 103 | type: "Outro", 104 | startTime: data.outro.start, 105 | endTime: data.outro.end, 106 | }); 107 | } 108 | 109 | sendResponse(container); 110 | }, 111 | }; 112 | -------------------------------------------------------------------------------- /src/scripts/module-index.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { 3 | archiveModule, 4 | copyLogo, 5 | getModuleFolder, 6 | getModuleIds, 7 | rewriteIndexFile, 8 | validateMetadata, 9 | } from "../utils/modules"; 10 | import ModuleSchema from "../schemas/module"; 11 | import path from "path"; 12 | import fs from "fs"; 13 | import { handlePath } from "../utils"; 14 | 15 | const args = process.argv.slice(2); 16 | 17 | if (args.length === 0) { 18 | console.log("No arguments provided."); 19 | process.exit(1); 20 | } 21 | 22 | const parseArg = (args: string[], providedArg: string) => { 23 | const argValue = args.find((arg) => arg.startsWith("--" + providedArg)); 24 | 25 | if (!argValue) { 26 | return null; 27 | } 28 | 29 | return argValue.split("=")[1]; 30 | }; 31 | 32 | const indexName = parseArg(args, "index-name"); 33 | const indexAuthor = parseArg(args, "index-author"); 34 | const indexURLPattern = parseArg(args, "index-url-pattern"); 35 | 36 | if (!indexName || !indexAuthor || !indexURLPattern) { 37 | console.log( 38 | "Index name (--index-name) and author (--index-author) and URL Pattern (--index-url-pattern) are required." 39 | ); 40 | process.exit(1); 41 | } 42 | 43 | const buildModule = async (module_id: string) => { 44 | const log = (message: string) => { 45 | console.log(`[${module_id}] ${message}`); 46 | }; 47 | 48 | if (!(await copyLogo(module_id))) { 49 | return log(`Failed to copy logo to dist`); 50 | } 51 | 52 | if (!(await rewriteIndexFile(module_id))) { 53 | log("Failed to rewrite index file"); 54 | 55 | return; 56 | } 57 | 58 | await archiveModule(module_id, { 59 | onError(err) { 60 | log(`Failed to build module (${err.message})`); 61 | }, 62 | }); 63 | }; 64 | 65 | const parseUrl = (module: z.infer) => { 66 | let url = indexURLPattern.replace("{{module_id}}", module.id); 67 | 68 | url = url.replace("{{module_name}}", module.name); 69 | url = url.replace("{{module_version}}", module.version); 70 | url = url.replace("{{module_type}}", module.type); 71 | url = url.replace("{{module_author}}", module.info.author); 72 | 73 | return url; 74 | }; 75 | 76 | const main = async () => { 77 | const moduleIds = await getModuleIds(); 78 | 79 | const outputFolder = handlePath("./output", process.cwd()); 80 | 81 | if (fs.existsSync(outputFolder)) { 82 | await fs.promises.rm(outputFolder, { recursive: true, force: true }); 83 | 84 | await fs.promises.mkdir(outputFolder); 85 | } 86 | 87 | await Promise.all(moduleIds.map(buildModule)); 88 | 89 | const indexJSON: { 90 | name: string; 91 | author: string; 92 | modules: z.infer[]; 93 | } = { 94 | author: indexAuthor, 95 | name: indexName, 96 | modules: [], 97 | }; 98 | 99 | for await (const module_id of moduleIds) { 100 | const moduleFolder = await getModuleFolder(module_id); 101 | const metadataFile = path.join(moduleFolder, "metadata.json"); 102 | 103 | const metadata = JSON.parse( 104 | await fs.promises.readFile(metadataFile, "utf-8") 105 | ); 106 | 107 | const validateResult = validateMetadata(metadata); 108 | 109 | if (!validateResult.success) { 110 | console.log( 111 | `[${module_id}] metadata.json file is not a valid metadata (${validateResult.error.message})` 112 | ); 113 | 114 | break; 115 | } 116 | 117 | const url = parseUrl(validateResult.data); 118 | 119 | indexJSON.modules.push({ 120 | ...validateResult.data, 121 | url, 122 | }); 123 | } 124 | 125 | fs.promises.writeFile( 126 | handlePath("./output/index.json", process.cwd()), 127 | JSON.stringify(indexJSON) 128 | ); 129 | 130 | console.log("Index file generated"); 131 | }; 132 | 133 | main(); 134 | -------------------------------------------------------------------------------- /src/utils/modules.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable quotes */ 3 | import fs from "fs"; 4 | import { handlePath, hasPath } from "."; 5 | import ModuleSchema from "../schemas/module"; 6 | import path from "path"; 7 | import archiver from "archiver"; 8 | 9 | export const getModuleIds = async () => { 10 | const modulesFolder = handlePath("./modules"); 11 | 12 | const data = await fs.promises.readdir(modulesFolder); 13 | 14 | return data; 15 | }; 16 | 17 | export const getModuleFolder = async (moduleId: string) => { 18 | const modulesFolder = handlePath(`./modules/${moduleId}`); 19 | 20 | if (!hasPath(modulesFolder)) { 21 | throw new Error(`Module ${moduleId} not found`); 22 | } 23 | 24 | return modulesFolder; 25 | }; 26 | 27 | export const readModuleFile = async (moduleId: string, fileName: string) => { 28 | const moduleFolder = await getModuleFolder(moduleId); 29 | 30 | const filePath = handlePath(`${moduleFolder}/${fileName}`); 31 | 32 | try { 33 | const data = await fs.promises.readFile(filePath, "utf-8"); 34 | 35 | return data; 36 | } catch (error) { 37 | throw new Error(`File ${fileName} not found`); 38 | } 39 | }; 40 | 41 | export const validateMetadata = (metadataFile: any) => { 42 | return ModuleSchema.safeParse(metadataFile); 43 | }; 44 | 45 | export const filterIndexFile = (indexFile: string) => { 46 | return indexFile 47 | .replace('"use strict";', "") 48 | .replace( 49 | 'Object.defineProperty(exports, "__esModule", { value: true });', 50 | "" 51 | ); 52 | }; 53 | 54 | export const copyLogo = async (module_id: string) => { 55 | try { 56 | const srcModuleFolder = handlePath( 57 | `./modules/${module_id}`, 58 | path.resolve(process.cwd(), "./src") 59 | ); 60 | const distModuleFolder = handlePath( 61 | `./modules/${module_id}`, 62 | path.resolve(process.cwd(), "./dist") 63 | ); 64 | 65 | const fromLogoPath = path.join(srcModuleFolder, "logo.png"); 66 | const toLogoPath = path.join(distModuleFolder, "logo.png"); 67 | 68 | await fs.promises.copyFile(fromLogoPath, toLogoPath); 69 | 70 | return true; 71 | } catch (err) { 72 | return false; 73 | } 74 | }; 75 | 76 | export const rewriteIndexFile = async (module_id: string) => { 77 | try { 78 | const moduleFolder = handlePath(`./modules/${module_id}`); 79 | const indexPath = path.join(moduleFolder, "index.js"); 80 | 81 | const indexFile = await fs.promises.readFile(indexPath, "utf-8"); 82 | 83 | const newFile = filterIndexFile(indexFile); 84 | 85 | await fs.promises.writeFile(indexPath, newFile); 86 | 87 | return true; 88 | } catch (err) { 89 | return false; 90 | } 91 | }; 92 | 93 | export const archiveModule = async ( 94 | module_id: string, 95 | { 96 | onError = () => {}, 97 | onFinished = () => {}, 98 | }: { 99 | onError?: (err: archiver.ArchiverError) => void; 100 | onFinished?: () => void; 101 | } 102 | ) => { 103 | const archive = archiver("zip"); 104 | 105 | const outputFolder = handlePath("./output", process.cwd()); 106 | const moduleFolder = handlePath(`./modules/${module_id}`); 107 | 108 | if (!hasPath(outputFolder)) { 109 | await fs.promises.mkdir(outputFolder); 110 | } 111 | 112 | archive.file(path.join(moduleFolder, "index.js"), { name: "index.js" }); 113 | archive.file(path.join(moduleFolder, "logo.png"), { name: "logo.png" }); 114 | archive.file(path.join(moduleFolder, "metadata.json"), { 115 | name: "metadata.json", 116 | }); 117 | 118 | const output = fs.createWriteStream( 119 | path.join(outputFolder, `${module_id}.kmodule`) 120 | ); 121 | 122 | archive.on("error", (err) => { 123 | onError(err); 124 | }); 125 | 126 | output.on("close", () => { 127 | onFinished(); 128 | }); 129 | 130 | archive.pipe(output); 131 | 132 | archive.finalize(); 133 | }; 134 | -------------------------------------------------------------------------------- /src/modules/animevietsub/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-nocheck 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | const anime = { 6 | hasGotBaseUrl: false, 7 | baseUrl: "", 8 | getBaseUrl: async () => { 9 | if (anime.hasGotBaseUrl) return; 10 | 11 | const { request } = await sendRequest("https://bit.ly/animevietsubtv"); 12 | 13 | let href = request.responseURL; 14 | 15 | if (href.endsWith("/")) href = href.slice(0, -1); 16 | 17 | anime.baseUrl = href; 18 | anime.hasGotBaseUrl = true; 19 | }, 20 | getId: async ({ media }) => { 21 | await anime.getBaseUrl(); 22 | 23 | const searchResults = await anime._totalSearch(media); 24 | 25 | sendResponse({ data: searchResults?.[0]?.id, extraData: {} }); 26 | }, 27 | getEpisodes: async ({ animeId }) => { 28 | await anime.getBaseUrl(); 29 | 30 | const { data: response } = await sendRequest( 31 | `${anime.baseUrl}/phim/a-a${animeId}/xem-phim.html` 32 | ); 33 | 34 | const parser = new DOMParser(); 35 | const doc = parser.parseFromString(response, "text/html"); 36 | 37 | const episodeElements = doc.querySelectorAll(".episode a"); 38 | const episodes = Array.from(episodeElements) 39 | .map((episodeEl) => { 40 | const name = episodeEl.getAttribute("title"); 41 | const number = anime._parseNumberFromName(name).toString(); 42 | const id = episodeEl.dataset.id; 43 | 44 | if (!name || !id) return; 45 | 46 | return { title: name, number, id }; 47 | }) 48 | .filter((a) => a); 49 | 50 | sendResponse(episodes); 51 | }, 52 | search: async ({ query }) => { 53 | await anime.getBaseUrl(); 54 | 55 | const searchResults = await anime._search(query); 56 | 57 | sendResponse(searchResults); 58 | }, 59 | async loadVideoServers({ episodeId }) { 60 | await anime.getBaseUrl(); 61 | 62 | const { data } = await sendRequest({ 63 | url: `${anime.baseUrl}/ajax/player?v=2019a`, 64 | data: `episodeId=${episodeId}&backup=1`, 65 | method: "post", 66 | headers: { 67 | "content-type": "application/x-www-form-urlencoded", 68 | }, 69 | }); 70 | 71 | const parser = new DOMParser(); 72 | const doc = parser.parseFromString(data?.html, "text/html"); 73 | 74 | const serverElements = doc.querySelectorAll("a"); 75 | const servers = Array.from(serverElements) 76 | .filter((el) => el.dataset.play === "api") 77 | .map((el) => { 78 | const id = el.dataset.id; 79 | const hash = el.dataset.href; 80 | const name = el.textContent.trim(); 81 | 82 | return { name, extraData: { id, hash } }; 83 | }); 84 | 85 | sendResponse(servers); 86 | }, 87 | 88 | async loadVideoContainer({ extraData }) { 89 | await anime.getBaseUrl(); 90 | 91 | const { id, hash } = extraData; 92 | 93 | const { data } = await sendRequest({ 94 | url: `${anime.baseUrl}/ajax/player?v=2019a`, 95 | data: `link=${hash}&id=${id}`, 96 | method: "post", 97 | headers: { 98 | "content-type": "application/x-www-form-urlencoded", 99 | }, 100 | }); 101 | 102 | const sources = data.link; 103 | 104 | sendResponse({ 105 | videos: sources.map((source) => ({ 106 | file: { 107 | url: !source.file.includes("https") 108 | ? `https://${source.file}` 109 | : source.file, 110 | headers: { 111 | referer: anime.baseUrl, 112 | }, 113 | }, 114 | quality: source.label, 115 | })), 116 | }); 117 | }, 118 | _search: async (query) => { 119 | await anime.getBaseUrl(); 120 | 121 | const { data: response } = await sendRequest( 122 | `${anime.baseUrl}/tim-kiem/${encodeURIComponent( 123 | query.toLowerCase() 124 | ).replaceAll("%20", "+")}/` 125 | ); 126 | 127 | const parser = new DOMParser(); 128 | const doc = parser.parseFromString(response, "text/html"); 129 | 130 | const searchResultsElements = doc.querySelectorAll(".TPostMv"); 131 | const searchResults = Array.from(searchResultsElements).map((el) => { 132 | const url = el.querySelector("a").getAttribute("href"); 133 | const id = anime._urlToId(url); 134 | const title = el.querySelector("h2").textContent; 135 | const thumbnail = el.querySelector("img").getAttribute("src"); 136 | 137 | return { 138 | id, 139 | title, 140 | thumbnail, 141 | }; 142 | }); 143 | 144 | return searchResults; 145 | }, 146 | _parseNumberFromName: (name) => { 147 | const numberName = name.replace("Tập ", ""); 148 | 149 | const number = parseInt(numberName); 150 | 151 | return isNaN(number) ? "Full" : number.toString(); 152 | }, 153 | _urlToId: (url) => { 154 | const splitted = url.split("/").filter((a) => a); 155 | const lastSplit = splitted[splitted.length - 1]; 156 | 157 | return lastSplit.split("-").slice(-1)[0].split("a")[1]; 158 | }, 159 | 160 | async _totalSearch(media) { 161 | const titles = Array.from( 162 | new Set([media?.title?.english, media?.title?.romaji]) 163 | ); 164 | 165 | if (!titles?.length) return []; 166 | 167 | for (const title of titles) { 168 | try { 169 | const searchResults = await anime._search(title); 170 | 171 | if (!searchResults?.length) continue; 172 | 173 | return searchResults; 174 | } catch (err) { 175 | console.error(err); 176 | } 177 | } 178 | 179 | return []; 180 | }, 181 | }; 182 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Added */ 4 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 5 | "preserveConstEnums": true /* Do not erase const enum declarations in generated code. */, 6 | "resolveJsonModule": true /* Include modules imported with '.json' extension. Requires TypeScript version 2.9 or later. */, 7 | "skipLibCheck": true, 8 | /* Basic Options */ 9 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 10 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 11 | "lib": [ 12 | "es5", 13 | "es6", 14 | "es2021" 15 | ] /* Specify library files to be included in the compilation. */, 16 | // "allowJs": true, /* Allow javascript files to be compiled. */ 17 | // "checkJs": true, /* Report errors in .js files. */ 18 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 19 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 20 | "sourceMap": true /* Generates corresponding '.map' file. */, 21 | // "outFile": "./", /* Concatenate and emit output to single file. */ 22 | "outDir": "dist" /* Redirect output structure to the directory. */, 23 | "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 24 | // "composite": true, /* Enable project compilation */ 25 | // "removeComments": true, /* Do not emit comments to output. */ 26 | // "noEmit": true, /* Do not emit outputs. */ 27 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 28 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 29 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 30 | 31 | /* Strict Type-Checking Options */ 32 | "strict": true /* Enable all strict type-checking options. */, 33 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 34 | "strictNullChecks": true /* Enable strict null checks. */, 35 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | "noUnusedLocals": true /* Report errors on unused locals. */, 42 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 43 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 44 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 45 | 46 | /* Module Resolution Options */ 47 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | "typeRoots": [ 52 | "node_modules/@types", 53 | "types", 54 | "src/module.d.ts" 55 | ] /* List of folders to include type definitions from. */, 56 | // "types": [], /* Type declaration files to be included in compilation. */ 57 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 58 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 59 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 60 | 61 | /* Source Map Options */ 62 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 63 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 64 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 65 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 66 | 67 | /* Experimental Options */ 68 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 69 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 70 | }, 71 | "exclude": ["node_modules", "test", "types"], 72 | "include": ["src/**/*.ts", "src/modules/**/*.json"] 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js TypeScript Template 2 | 3 | [![Package Version][package-image]][package-url] 4 | [![Dependencies Status][dependencies-image]][dependencies-url] 5 | [![Build Status][build-image]][build-url] 6 | [![Coverage Status][coverage-image]][coverage-url] 7 | [![Open Issues][issues-image]][issues-url] 8 | [![Commitizen Friendly][commitizen-image]][commitizen-url] 9 | 10 | A complete Node.js project template using TypeScript and following general best practices. It allows you to skip the tedious details for the following: 11 | 12 | * Adding and configuring TypeScript support. 13 | * Enabling TypeScript linting. 14 | * Setting up unit tests and code coverage reports. 15 | * Creating an NPM package for your project. 16 | * Managing ignored files for Git and NPM. 17 | 18 | Once you've enabled CI, test coverage, and dependency reports for your project, this README.md file shows how to add the badges shown above. This project template even enables automated changelog generation as long as you follow [Conventional Commits](https://conventionalcommits.org), which is made simple through the included [Commitizen CLI](http://commitizen.github.io/cz-cli/). 19 | 20 | ## Contents 21 | 22 | * [Project Creation](#project-creation) 23 | * [Rebranding](#rebranding) 24 | * [Managing Your Project](#managing-your-project) 25 | * [Initial Publish](#initial-publish) 26 | * [Recommended Development Workflow](#recommended-development-workflow) 27 | * [Publishing to NPMJS](#publishing-to-npmjs) 28 | * [Contributing](#contributing) 29 | 30 | ## Project Creation 31 | 32 | Clone this repo into the directory you want to use for your new project, delete the Git history, and then reinit as a fresh Git repo: 33 | 34 | ```bash 35 | $ git clone https://github.com/chriswells0/node-typescript-template.git 36 | $ cd 37 | $ rm -rf ./.git/ 38 | $ git init 39 | $ npm install 40 | ``` 41 | 42 | ## Rebranding 43 | 44 | It's a common practice to prefix the source code project name with `node-` to make it clear on GitHub that it's a Node.js project while omitting that prefix in the NPM project since it's understood on npmjs.com. Thus, the order of these replacements matter. 45 | 46 | Be sure to check both [GitHub](https://github.com) and [NPMJS](https://www.npmjs.com) to verify that your project name isn't taken before starting! 47 | 48 | Use exact searches to perform the following replacements throughout this project for the most efficient rebranding process: 49 | 50 | 1. Replace my name with yours: `Chris Wells` 51 | 2. Replace my website URL with yours: `https://chriswells.io` 52 | 3. Replace my *GitHub* username and project name with yours: `chriswells0/node-typescript-template` 53 | 4. Replace my *NPM* project name with yours: `typescript-template` 54 | 5. Update [package.json](package.json): 55 | * Change `description` to suit your project. 56 | * Update the `keywords` list. 57 | * In the `author` section, add `email` if you want to include yours. 58 | 6. If you prefer something other than the [BSD 3-Clause License](https://opensource.org/licenses/BSD-3-Clause), replace the entire contents of [LICENSE](LICENSE) as appropriate. 59 | 7. Update this README.md file to describe your project. 60 | 61 | ## Managing Your Project 62 | 63 | Before committing to a project based on this template, it's recommended that you read about [Conventional Commits](https://conventionalcommits.org) and install [Commitizen CLI](http://commitizen.github.io/cz-cli/) globally. 64 | 65 | ### Initial Publish 66 | 67 | Some additional steps need to be performed for a new project. Specifically, you'll need to: 68 | 69 | 1. Create your project on GitHub (do not add a README, .gitignore, or license). 70 | 2. Add the initial files to the repo: 71 | ```bash 72 | $ git add . 73 | $ git cz 74 | $ git remote add origin git@github.com:/ 75 | $ git push -u origin master 76 | ``` 77 | 3. Create accounts on the following sites and add your new GitHub project to them. The project is preconfigured, so it should "just work" with these tools. 78 | * GitHub Actions for continuous integration. 79 | * [Coveralls](https://coveralls.io) for unit test coverage verification. 80 | 4. Check the "Actions" tab on the GitHub repo and wait for the Node.js CI build to complete. 81 | 5. Publish your package to NPMJS: `npm publish` 82 | 83 | ### Development Workflow 84 | 85 | #### Hot reload 86 | Run `npm run serve` to start your development workflow with hot reload. 87 | 88 | #### Build, test, deploy 89 | 90 | These steps need to be performed whenever you make changes: 91 | 92 | 0. Write awesome code in the `src` directory. 93 | 1. Build (clean, lint, and transpile): `npm run build` 94 | 2. Create unit tests in the `test` directory. If your code is not awesome, you may have to fix some things here. 95 | 3. Verify code coverage: `npm run cover:check` 96 | 4. Commit your changes using `git add` and `git cz` 97 | 5. Push to GitHub using `git push` and wait for the CI builds to complete. Again, success depends upon the awesomeness of your code. 98 | 99 | ### NPMJS Updates 100 | 101 | Follow these steps to update your NPM package: 102 | 103 | 0. Perform all development workflow steps including pushing to GitHub in order to verify the CI builds. You don't want to publish a broken package! 104 | 1. Check to see if this qualifies as a major, minor, or patch release: `npm run changelog:unreleased` 105 | 2. Bump the NPM version following [Semantic Versioning](https://semver.org) by using **one** of these approaches: 106 | * Specify major, minor, or patch and let NPM bump it: `npm version [major | minor | patch] -m "chore(release): Bump version to %s."` 107 | * Explicitly provide the version number such as 1.0.0: `npm version 1.0.0 -m "chore(release): Bump version to %s."` 108 | 3. The push to GitHub is automated, so wait for the CI builds to finish. 109 | 4. Publishing the new version to NPMJS is also automated, but you must create a secret named `NPM_TOKEN` on your project. 110 | 5. Manually create a new release in GitHub based on the automatically created tag. 111 | 112 | ## Contributing 113 | 114 | This section is here as a reminder for you to explain to your users how to contribute to the projects you create from this template. 115 | 116 | [build-image]: https://img.shields.io/github/actions/workflow/status/chriswells0/node-typescript-template/ci-build.yaml?branch=master 117 | [build-url]: https://github.com/chriswells0/node-typescript-template/actions/workflows/ci-build.yaml 118 | [commitizen-image]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg 119 | [commitizen-url]: http://commitizen.github.io/cz-cli 120 | [coverage-image]: https://coveralls.io/repos/github/chriswells0/node-typescript-template/badge.svg?branch=master 121 | [coverage-url]: https://coveralls.io/github/chriswells0/node-typescript-template?branch=master 122 | [dependencies-image]: https://img.shields.io/librariesio/release/npm/typescript-template 123 | [dependencies-url]: https://www.npmjs.com/package/typescript-template?activeTab=dependencies 124 | [issues-image]: https://img.shields.io/github/issues/chriswells0/node-typescript-template.svg?style=popout 125 | [issues-url]: https://github.com/chriswells0/node-typescript-template/issues 126 | [package-image]: https://img.shields.io/npm/v/typescript-template 127 | [package-url]: https://www.npmjs.com/package/typescript-template 128 | [project-url]: https://github.com/chriswells0/node-typescript-template 129 | -------------------------------------------------------------------------------- /src/modules/animehay/index.ts: -------------------------------------------------------------------------------- 1 | import { Anime, Episode, SearchResult, VideoServer } from "../../types"; 2 | 3 | interface WindowAnime extends Anime { 4 | baseUrl: string; 5 | hasGotBaseUrl: boolean; 6 | _totalSearch: (media: { 7 | id: number; 8 | title: { 9 | romaji: string; 10 | english: string; 11 | userPreferred: string; 12 | native: string; 13 | }; 14 | }) => Promise; 15 | _search: ( 16 | query: string, 17 | shouldRemoveDuplicates?: boolean 18 | ) => Promise; 19 | _urlToId: (url: string) => string; 20 | _getFirePlayerUrl: (url: string) => Promise; 21 | getBaseURL: () => Promise; 22 | } 23 | 24 | const anime: WindowAnime = { 25 | hasGotBaseUrl: false, 26 | baseUrl: "", 27 | async getBaseURL() { 28 | if (anime.hasGotBaseUrl) return; 29 | 30 | const { data: text } = await sendRequest("https://animehay.tv"); 31 | 32 | const parser = new DOMParser(); 33 | const doc = parser.parseFromString(text, "text/html"); 34 | 35 | let href = doc.querySelector(".bt-link")?.getAttribute("href"); 36 | 37 | if (!href) return; 38 | 39 | if (href.endsWith("/")) href = href.slice(0, -1); 40 | 41 | anime.baseUrl = href; 42 | anime.hasGotBaseUrl = true; 43 | }, 44 | getId: async ({ media }) => { 45 | await anime.getBaseURL(); 46 | 47 | const searchResults = await anime._totalSearch(media); 48 | 49 | sendResponse({ 50 | data: searchResults?.[0]?.id, 51 | }); 52 | }, 53 | 54 | getEpisodes: async ({ animeId }) => { 55 | await anime.getBaseURL(); 56 | 57 | const { data: text } = await sendRequest( 58 | `${anime.baseUrl}/thong-tin-phim/a-${animeId}.html` 59 | ); 60 | 61 | const parser = new DOMParser(); 62 | const doc = parser.parseFromString(text, "text/html"); 63 | 64 | const episodeElements = Array.from( 65 | doc.querySelectorAll(".list-item-episode > a") 66 | ); 67 | 68 | const episodeList: Episode[] = episodeElements 69 | .map((episodeEl) => { 70 | const href = episodeEl.getAttribute("href"); 71 | 72 | if (!href) return null; 73 | 74 | const sourceEpisodeId = (href.match(/-(\d+)\.html$/) || [])[1] || null; 75 | const name = episodeEl.textContent?.trim(); 76 | 77 | if (!sourceEpisodeId || !name) return null; 78 | 79 | const number = parseInt(name)?.toString(); 80 | 81 | if (!sourceEpisodeId || !number) return null; 82 | 83 | return { 84 | id: sourceEpisodeId, 85 | number, 86 | }; 87 | }) 88 | .filter(Boolean) 89 | .sort((a, b) => Number(a.number) - Number(b.number)); 90 | 91 | sendResponse(episodeList); 92 | }, 93 | 94 | loadVideoServers: async ({ episodeId }) => { 95 | await anime.getBaseURL(); 96 | 97 | const { data: text } = await sendRequest( 98 | `${anime.baseUrl}/xem-phim/a-${episodeId}.html` 99 | ); 100 | 101 | const pattern = /(?<=['"(])(https?:\/\/\S+)(?=['")])/gi; 102 | const matches: string[] = Array.from(text.matchAll(pattern)); 103 | 104 | const servers: VideoServer[] = []; 105 | 106 | for (const match of matches) { 107 | const url = match[0]; 108 | let name = ""; 109 | 110 | if (url.includes("cdninstagram.com")) { 111 | name = "FBO"; 112 | } else if (url.includes("suckplayer.xyz")) { 113 | name = "VPRO"; 114 | } else if (url.includes("rapovideo.xyz")) { 115 | name = "Tik"; 116 | } else { 117 | continue; 118 | } 119 | 120 | servers.push({ 121 | name, 122 | extraData: { 123 | link: url, 124 | }, 125 | }); 126 | } 127 | 128 | sendResponse(servers); 129 | }, 130 | 131 | loadVideoContainer: async ({ name, extraData }) => { 132 | await anime.getBaseURL(); 133 | 134 | const { link } = extraData as { link: string }; 135 | 136 | if (name === "FBO" || name === "Tik") { 137 | sendResponse({ 138 | videos: [ 139 | { 140 | quality: "720p", 141 | file: { 142 | url: link, 143 | }, 144 | }, 145 | ], 146 | }); 147 | 148 | return; 149 | } 150 | 151 | if (name === "VPRO") { 152 | const url = await anime._getFirePlayerUrl(link); 153 | 154 | sendResponse({ 155 | videos: [ 156 | { 157 | quality: "720p", 158 | file: { 159 | url: url, 160 | headers: { 161 | Origin: "https://suckplayer.xyz", 162 | }, 163 | }, 164 | }, 165 | ], 166 | }); 167 | 168 | return; 169 | } 170 | }, 171 | 172 | async _getFirePlayerUrl(url: string) { 173 | const id = url.split("/")[4]; 174 | 175 | const { data } = await sendRequest({ 176 | url: `https://suckplayer.xyz/player/index.php?data=${id}&do=getVideo`, 177 | method: "POST", 178 | headers: { 179 | "X-Requested-With": "XMLHttpRequest", 180 | "Content-Type": "application/x-www-form-urlencoded", 181 | Origin: "https://suckplayer.xyz", 182 | }, 183 | data: `r=${encodeURIComponent(anime.baseUrl)}&hash=${id}`, 184 | }); 185 | 186 | const link = data.securedLink; 187 | 188 | return link; 189 | }, 190 | 191 | search: async ({ query }) => { 192 | await anime.getBaseURL(); 193 | 194 | const searchResults = await anime._search(query); 195 | 196 | sendResponse(searchResults); 197 | }, 198 | 199 | _search: async (query) => { 200 | await anime.getBaseURL(); 201 | 202 | const { data } = await sendRequest({ 203 | url: `${anime.baseUrl}/api`, 204 | headers: { 205 | referrer: anime.baseUrl, 206 | }, 207 | data: { action: "live_search", keyword: query }, 208 | method: "POST", 209 | }); 210 | 211 | if (!data?.result) return []; 212 | 213 | const parser = new DOMParser(); 214 | const doc = parser.parseFromString(data.result, "text/html"); 215 | 216 | const linkElements = Array.from( 217 | doc.querySelectorAll(`a[href*="/thong-tin-phim/"]`) 218 | ); 219 | 220 | const searchResults: SearchResult[] = linkElements 221 | .map((element) => { 222 | const href = element.getAttribute("href"); 223 | const thumbnail = element.querySelector("img")?.getAttribute("src"); 224 | const title = element.querySelector(".fw-500")?.textContent?.trim(); 225 | 226 | if (!href || !thumbnail || !title) return null; 227 | 228 | const id = anime._urlToId(href); 229 | 230 | return { thumbnail, title, id }; 231 | }) 232 | .filter(Boolean); 233 | 234 | return searchResults; 235 | }, 236 | async _totalSearch(media) { 237 | const titles = Array.from( 238 | new Set([media?.title?.english, media?.title?.romaji]) 239 | ); 240 | 241 | if (!titles?.length) return []; 242 | 243 | for (const title of titles) { 244 | try { 245 | const searchResults = await anime._search(title); 246 | 247 | if (!searchResults?.length) continue; 248 | 249 | return searchResults; 250 | } catch (err) { 251 | console.error(err); 252 | } 253 | } 254 | 255 | return []; 256 | }, 257 | _urlToId: (url) => { 258 | const splitted = url.split("/"); 259 | const lastSplit = splitted[splitted.length - 1]; 260 | 261 | return lastSplit.split("-").slice(-1)[0].replace(".html", ""); 262 | }, 263 | }; 264 | -------------------------------------------------------------------------------- /src/scripts/module-test.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ExecuteCodeFunction, createExecutor } from "../utils/executor"; 3 | import { inputModuleId } from "../utils/module-cli"; 4 | import { filterIndexFile, getModuleFolder } from "../utils/modules"; 5 | import { hasPath } from "../utils"; 6 | import ora from "ora"; 7 | import { z } from "zod"; 8 | import fs from "fs/promises"; 9 | import { EpisodeSchema } from "../schemas/episode"; 10 | import { SearchResultSchema } from "../schemas/search-result"; 11 | import { VideoServerSchema } from "../schemas/video-server"; 12 | import { VideoContainerSchema } from "../schemas/video-container"; 13 | import prompts from "prompts"; 14 | import axios from "axios"; 15 | 16 | const GetIdResultSchema = z.object({ 17 | data: z.string(), 18 | extraData: z.record(z.string()).nullable().optional(), 19 | }); 20 | const GetEpisodesResultSchema = z.array(EpisodeSchema).nonempty(); 21 | const GetSearchResultSchema = z.array(SearchResultSchema); 22 | const GetVideoServersResultSchema = z.array(VideoServerSchema).nonempty(); 23 | 24 | const checkIndexFile = (moduleFolder: string) => { 25 | const file = path.join(moduleFolder, "index.js"); 26 | const spinner = ora("Is there index.js file?").start(); 27 | 28 | if (!hasPath(file)) { 29 | spinner.fail("index.js file not found"); 30 | return false; 31 | } 32 | 33 | spinner.succeed("index.js file found"); 34 | 35 | return true; 36 | }; 37 | 38 | const checkGetIdFunction = async ( 39 | executeCode: ExecuteCodeFunction, 40 | params: any 41 | ) => { 42 | const spinner = ora("anime.getId").start(); 43 | 44 | const data = await executeCode>( 45 | "anime.getId", 46 | params 47 | ); 48 | 49 | const validation = GetIdResultSchema.safeParse(data); 50 | 51 | if (!validation.success) { 52 | spinner.fail(`anime.getId (${validation.error.message})`); 53 | 54 | console.error(data); 55 | 56 | return undefined; 57 | } 58 | 59 | spinner.succeed(); 60 | 61 | return validation.data; 62 | }; 63 | 64 | const checkSearchFunction = async (executeCode: ExecuteCodeFunction) => { 65 | const spinner = ora("anime.search").start(); 66 | 67 | const data = await executeCode>( 68 | "anime.search", 69 | { query: "one piece" } 70 | ); 71 | 72 | const validation = GetSearchResultSchema.safeParse(data); 73 | 74 | if (!validation.success) { 75 | spinner.fail(`anime.search (${validation.error.message})`); 76 | 77 | console.error(data); 78 | 79 | return undefined; 80 | } 81 | 82 | spinner.succeed(); 83 | 84 | return true; 85 | }; 86 | 87 | const checkGetEpisodesFunction = async ( 88 | input: z.infer, 89 | executeCode: ExecuteCodeFunction 90 | ) => { 91 | const spinner = ora("anime.getEpisodes").start(); 92 | 93 | const data = await executeCode>( 94 | "anime.getEpisodes", 95 | { animeId: input.data, extraData: input.extraData } 96 | ); 97 | 98 | const validation = GetEpisodesResultSchema.safeParse(data); 99 | 100 | if (!validation.success) { 101 | spinner.fail(`anime.getEpisodes (${validation.error.message})`); 102 | 103 | console.error(data); 104 | 105 | return undefined; 106 | } 107 | 108 | spinner.succeed(); 109 | 110 | return validation.data; 111 | }; 112 | 113 | const checkVideoServersFunction = async ( 114 | input: z.infer, 115 | executeCode: ExecuteCodeFunction 116 | ) => { 117 | const spinner = ora("anime.loadVideoServers").start(); 118 | 119 | const data = await executeCode>( 120 | "anime.loadVideoServers", 121 | { 122 | extraData: input.extra, 123 | episodeId: input.id, 124 | } 125 | ); 126 | 127 | const validation = GetVideoServersResultSchema.safeParse(data); 128 | 129 | if (!validation.success) { 130 | spinner.fail(`anime.loadVideoServers (${validation.error.message})`); 131 | 132 | console.error(data); 133 | 134 | return undefined; 135 | } 136 | 137 | spinner.succeed(); 138 | 139 | return validation.data; 140 | }; 141 | 142 | const checkVideoContainer = async ( 143 | input: z.infer, 144 | executeCode: ExecuteCodeFunction 145 | ) => { 146 | const spinner = ora("anime.loadVideoContainer").start(); 147 | 148 | const data = await executeCode>( 149 | "anime.loadVideoContainer", 150 | input 151 | ); 152 | 153 | const validation = VideoContainerSchema.safeParse(data); 154 | 155 | if (!validation.success) { 156 | spinner.fail(`anime.loadVideoContainer (${validation.error.message})`); 157 | 158 | console.error(data); 159 | 160 | return undefined; 161 | } 162 | 163 | spinner.succeed(); 164 | 165 | return validation.data; 166 | }; 167 | 168 | const getAnilistMedia = async (mediaId: number) => { 169 | const spinner = ora("Get media from Anilist").start(); 170 | 171 | try { 172 | const { data } = await axios({ 173 | url: "https://graphql.anilist.co", 174 | method: "post", 175 | data: { 176 | query: ` 177 | query ($id: Int) { 178 | Media (id: $id, type: ANIME) { 179 | id 180 | title { 181 | romaji 182 | english 183 | native 184 | userPreferred 185 | } 186 | } 187 | } 188 | `, 189 | variables: { 190 | id: mediaId, 191 | }, 192 | }, 193 | }); 194 | 195 | spinner.succeed(); 196 | 197 | return data?.data?.Media; 198 | } catch (err) { 199 | spinner.fail(); 200 | 201 | console.error(err); 202 | 203 | return undefined; 204 | } 205 | }; 206 | 207 | const main = async () => { 208 | const module_id = await inputModuleId(); 209 | 210 | const { media_id } = await prompts({ 211 | name: "media_id", 212 | message: "Write the media id (AniList) to test the module", 213 | type: "number", 214 | }); 215 | 216 | const media = await getAnilistMedia(media_id); 217 | 218 | console.log(media); 219 | 220 | const moduleFolder = await getModuleFolder(module_id); 221 | const indexFile = path.join(moduleFolder, "index.js"); 222 | 223 | if (!checkIndexFile(moduleFolder)) { 224 | return; 225 | } 226 | 227 | const indexFileContent = filterIndexFile( 228 | await fs.readFile(indexFile, "utf-8") 229 | ); 230 | 231 | if (!indexFileContent) { 232 | console.log("index.js file is empty"); 233 | 234 | return; 235 | } 236 | 237 | const { executeCode, injectCode } = createExecutor(); 238 | 239 | injectCode(indexFileContent); 240 | 241 | const animeId = await checkGetIdFunction(executeCode, { media }); 242 | 243 | if (!animeId) { 244 | return; 245 | } 246 | 247 | const episodes = await checkGetEpisodesFunction(animeId, executeCode); 248 | 249 | if (!episodes) { 250 | return; 251 | } 252 | 253 | const videoServers = await checkVideoServersFunction( 254 | episodes[0], 255 | executeCode 256 | ); 257 | 258 | if (!videoServers) { 259 | return; 260 | } 261 | 262 | const videoContainer = await checkVideoContainer( 263 | videoServers[0], 264 | executeCode 265 | ); 266 | 267 | if (!videoContainer) { 268 | return; 269 | } 270 | 271 | const searchResults = await checkSearchFunction(executeCode); 272 | 273 | if (!searchResults) { 274 | return; 275 | } 276 | 277 | console.log(`Module ${module_id} passed all tests`); 278 | }; 279 | 280 | main(); 281 | -------------------------------------------------------------------------------- /src/modules/anizone/index.ts: -------------------------------------------------------------------------------- 1 | import type { Anime, SearchResult } from "../../types"; 2 | 3 | interface WindowAnime extends Anime { 4 | baseUrl: string; 5 | 6 | // Helper functions 7 | _parseBetween: (text: string, start: string, end: string) => string; 8 | _removeDuplicates: (arr: T[], comparator: (a: T, b: T) => boolean) => T[]; 9 | _getNextEpisodes: ( 10 | animeId: string, 11 | snapshot: string, 12 | csrf: string 13 | ) => Promise; 14 | _parseSearchResults: (html: string) => SearchResult[]; 15 | _parseEpisodes: (html: string, animeId: string) => SearchResult[]; 16 | 17 | _totalSearch: (media: { 18 | id: number; 19 | title: { 20 | romaji: string; 21 | english: string; 22 | userPreferred: string; 23 | native: string; 24 | }; 25 | }) => Promise; 26 | 27 | _search: (query: string) => Promise; 28 | } 29 | 30 | const anime: WindowAnime = { 31 | baseUrl: "https://anizone.to", 32 | 33 | getId: async ({ media }) => { 34 | const searchResults = await anime._totalSearch(media); 35 | 36 | const searchResultWithSameId = searchResults.find( 37 | (result) => Number(result.extra?.anilistId) === media.id 38 | ); 39 | 40 | if (searchResultWithSameId) { 41 | return sendResponse({ 42 | data: searchResultWithSameId.id, 43 | extraData: searchResultWithSameId.extra, 44 | }); 45 | } 46 | 47 | sendResponse({ 48 | data: searchResults?.[0]?.id, 49 | extraData: searchResults?.[0]?.extra, 50 | }); 51 | }, 52 | 53 | getEpisodes: async ({ animeId }) => { 54 | const { data: html } = await sendRequest( 55 | `${anime.baseUrl}/anime/${animeId}` 56 | ); 57 | 58 | const episodes = anime._parseEpisodes(html, animeId); 59 | 60 | const snapshot = anime._parseBetween(html, 'wire:snapshot="', '"'); 61 | const csrf = anime._parseBetween( 62 | html, 63 | ' { 76 | sendResponse(await anime._search(query)); 77 | }, 78 | 79 | _search: async (query) => { 80 | const { data: html } = await sendRequest( 81 | `${anime.baseUrl}/anime?search=${encodeURIComponent(query)}` 82 | ); 83 | 84 | return anime._parseSearchResults(html); 85 | }, 86 | 87 | loadVideoServers: async ({ episodeId }) => { 88 | const [animeId, number] = episodeId.split("-"); 89 | 90 | sendResponse([ 91 | { 92 | embed: "", 93 | name: "Server", 94 | extraData: { 95 | animeId, 96 | number, 97 | }, 98 | }, 99 | ]); 100 | }, 101 | 102 | loadVideoContainer: async ({ extraData }) => { 103 | const animeId = extraData?.animeId; 104 | const number = extraData?.number; 105 | 106 | if (!animeId) return sendResponse({ videos: [], subtitles: [] }); 107 | 108 | const { data: html } = await sendRequest( 109 | `${anime.baseUrl}/anime/${animeId}/${number}` 110 | ); 111 | 112 | const parser = new DOMParser().parseFromString(html, "text/html"); 113 | 114 | const source = parser.querySelector("media-player")?.getAttribute("src"); 115 | 116 | const tracks = Array.from( 117 | parser.querySelectorAll("media-player track") 118 | ).map((track) => ({ 119 | file: { url: track.getAttribute("src") }, 120 | language: track.getAttribute("label"), 121 | })); 122 | 123 | sendResponse({ 124 | videos: [ 125 | { 126 | format: "hls", 127 | file: { url: source }, 128 | }, 129 | ], 130 | subtitles: tracks, 131 | }); 132 | }, 133 | 134 | _parseBetween: (text, start, end) => { 135 | const strArr = text.split(start); 136 | return strArr[1]?.split(end)[0] || ""; 137 | }, 138 | 139 | _removeDuplicates: (arr, comparator) => { 140 | return arr.filter( 141 | (item, index, self) => 142 | self.findIndex((t) => comparator(t, item)) === index 143 | ); 144 | }, 145 | 146 | _parseEpisodes: (html, animeId) => { 147 | const doc = new DOMParser().parseFromString(html, "text/html"); 148 | 149 | return Array.from(doc.querySelectorAll("ul li")).map((item) => { 150 | const id = item 151 | .querySelector("a") 152 | ?.getAttribute("href") 153 | ?.split("/") 154 | .pop(); 155 | const thumbnail = anime._parseBetween( 156 | item.querySelector("img")?.getAttribute(":src") || "", 157 | "hvr || fcs ? '", 158 | "'" 159 | ); 160 | const title = item.querySelector("h3")?.textContent?.trim(); 161 | 162 | return { 163 | id: `${animeId}-${id}`, 164 | number: id, 165 | title, 166 | thumbnail, 167 | }; 168 | }) as SearchResult[]; 169 | }, 170 | 171 | _getNextEpisodes: async (animeId, snapshot, csrf) => { 172 | const { data: response } = await sendRequest<{ 173 | components: Array<{ 174 | snapshot: string; 175 | effects: { 176 | html: string; 177 | }; 178 | }>; 179 | }>({ 180 | url: `${anime.baseUrl}/livewire/update`, 181 | method: "POST", 182 | headers: { 183 | "Content-Type": "application/json", 184 | Referer: `${anime.baseUrl}/anime/${animeId}`, 185 | }, 186 | data: { 187 | _token: csrf, 188 | components: [ 189 | { 190 | snapshot, 191 | updates: {}, 192 | calls: [{ path: "", method: "loadMore", params: [] }], 193 | }, 194 | ], 195 | }, 196 | }); 197 | 198 | if (!response?.components?.[0]?.effects?.html) return []; 199 | 200 | const newSnapshot = response.components[0].snapshot; 201 | const newEpisodes = anime._parseEpisodes( 202 | response.components[0].effects.html, 203 | animeId 204 | ); 205 | 206 | if ( 207 | response.components[0].effects.html.includes( 208 | `x-intersect="$wire.loadMore()"` 209 | ) 210 | ) { 211 | const moreEpisodes = await anime._getNextEpisodes( 212 | animeId, 213 | newSnapshot, 214 | csrf 215 | ); 216 | return [...newEpisodes, ...moreEpisodes]; 217 | } 218 | 219 | return newEpisodes; 220 | }, 221 | 222 | _parseSearchResults: (html: string) => { 223 | const doc = new DOMParser().parseFromString(html, "text/html"); 224 | return anime._removeDuplicates( 225 | Array.from(doc.querySelectorAll(".grid div.relative")).map((item) => { 226 | const image = item.querySelector(".absolute img")?.getAttribute("src"); 227 | const name = item 228 | .querySelector(".relative a") 229 | ?.textContent?.trim() 230 | .split("\n")[0]; 231 | const href = item.querySelector("a")?.getAttribute("href"); 232 | const id = href?.split("/").pop(); 233 | 234 | return { 235 | id, 236 | title: name, 237 | thumbnail: image, 238 | }; 239 | }), 240 | (a, b) => a.id === b.id 241 | ) as SearchResult[]; 242 | }, 243 | 244 | async _totalSearch(media) { 245 | const titles = Array.from( 246 | new Set([media?.title?.english, media?.title?.romaji]) 247 | ); 248 | 249 | if (!titles?.length) return []; 250 | 251 | for (const title of titles) { 252 | try { 253 | const searchResults = await anime._search(title); 254 | 255 | if (!searchResults?.length) continue; 256 | 257 | return searchResults; 258 | } catch (err) { 259 | console.error(err); 260 | } 261 | } 262 | 263 | return []; 264 | }, 265 | }; 266 | -------------------------------------------------------------------------------- /src/modules/animepahe/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-global-assign */ 2 | import { Anime, Episode, SearchResult, VideoServer } from "../../types"; 3 | 4 | interface WindowAnime extends Anime { 5 | baseUrl: string; 6 | _totalSearch: (media: { 7 | id: number; 8 | title: { 9 | romaji: string; 10 | english: string; 11 | userPreferred: string; 12 | native: string; 13 | }; 14 | }) => Promise; 15 | _search: ( 16 | query: string, 17 | shouldRemoveDuplicates?: boolean 18 | ) => Promise; 19 | _packer: { 20 | detect: (text: string) => boolean; 21 | get_chunks: (str: string) => string[]; 22 | unpack: (packed: string) => string; 23 | unpack_chunk: (packed: string) => string; 24 | }; 25 | _loadAllEpisodes: (animeSession: string) => Promise; 26 | _parseBetween: (text: string, start: string, end: string) => string; 27 | } 28 | 29 | const anime: WindowAnime = { 30 | baseUrl: "https://animepahe.ru", 31 | 32 | getId: async ({ media }) => { 33 | const searchResults = await anime._totalSearch(media); 34 | 35 | sendResponse({ 36 | data: searchResults?.[0]?.id, 37 | }); 38 | }, 39 | 40 | getEpisodes: async ({ animeId }) => { 41 | const { data: response } = await sendRequest({ 42 | url: `${anime.baseUrl}/a/${animeId}`, 43 | headers: { 44 | cookie: "__ddgid_=; __ddg2_=; __ddg1_=", 45 | }, 46 | withCredentials: false, 47 | }); 48 | 49 | const animeSession = anime._parseBetween(response, 'let id = "', '"'); 50 | 51 | const rawEpisodes = await anime._loadAllEpisodes(animeSession); 52 | 53 | const episodes = rawEpisodes.map((episode) => ({ 54 | id: episode.session, 55 | number: episode.episode.toString(), 56 | thumbnail: episode.snapshot, 57 | isFiller: !!episode.filler, 58 | extra: { 59 | animeSession, 60 | }, 61 | })); 62 | 63 | sendResponse(episodes); 64 | }, 65 | 66 | loadVideoServers: async ({ episodeId, extraData }) => { 67 | if (!extraData?.animeSession) throw new Error("ID not found"); 68 | 69 | const url = `${anime.baseUrl}/play/${extraData.animeSession}/${episodeId}`; 70 | 71 | const { data: response } = await sendRequest({ 72 | url, 73 | headers: { 74 | cookie: "__ddgid_=; __ddg2_=; __ddg1_=", 75 | }, 76 | withCredentials: false, 77 | }); 78 | 79 | const parser = new DOMParser(); 80 | const doc = parser.parseFromString(response, "text/html"); 81 | 82 | const serverItems = Array.from( 83 | doc.querySelectorAll("#resolutionMenu button") 84 | ); 85 | 86 | const servers: VideoServer[] = serverItems 87 | .map((el) => { 88 | if (!el.textContent) return null; 89 | 90 | const embed = el.getAttribute("data-src"); 91 | 92 | if (!embed) return null; 93 | 94 | return { 95 | extraData: { 96 | embed, 97 | }, 98 | name: el.textContent.trim(), 99 | }; 100 | }) 101 | .filter(Boolean); 102 | 103 | sendResponse(servers); 104 | }, 105 | 106 | loadVideoContainer: async ({ extraData }) => { 107 | if (!extraData?.embed) throw new Error("Embed not found"); 108 | 109 | const { data: response } = await sendRequest({ 110 | url: extraData.embed, 111 | headers: { 112 | referer: "https://kwik.si/", 113 | cookie: "__ddgid_=; __ddg2_=; __ddg1_=", 114 | }, 115 | withCredentials: false, 116 | }); 117 | 118 | const packedString = 119 | "eval(function(p,a,c,k,e,d)" + 120 | anime._parseBetween( 121 | response, 122 | "" 124 | ); 125 | 126 | const unpacked = anime._packer.unpack(packedString); 127 | 128 | const stream = anime._parseBetween(unpacked, "const source='", "';"); 129 | 130 | sendResponse({ 131 | videos: [ 132 | { 133 | file: { 134 | url: stream, 135 | headers: { 136 | referer: "https://kwik.si/", 137 | }, 138 | }, 139 | }, 140 | ], 141 | }); 142 | }, 143 | 144 | search: async ({ query }) => { 145 | const searchResults = await anime._search(query); 146 | 147 | sendResponse(searchResults); 148 | }, 149 | 150 | _loadAllEpisodes(animeSession) { 151 | const episodes: Episode[] = []; 152 | 153 | const load = async (page = 1): Promise => { 154 | const { data: episodeResponse } = await sendRequest({ 155 | url: `${anime.baseUrl}/api?m=release&id=${animeSession}&sort=episode_asc&page=${page}`, 156 | headers: { 157 | cookie: "__ddgid_=; __ddg2_=; __ddg1_=", 158 | }, 159 | withCredentials: false, 160 | }); 161 | 162 | if (episodeResponse?.data?.length) { 163 | episodes.push(...episodeResponse.data); 164 | } 165 | 166 | if (!episodeResponse?.next_page_url) return episodes; 167 | 168 | return load(page + 1); 169 | }; 170 | 171 | return load(1); 172 | }, 173 | _search: async (query) => { 174 | const encodedQuery = encodeURIComponent(query); 175 | 176 | const { data: response } = await sendRequest({ 177 | url: `${anime.baseUrl}/api?m=search&q=${encodedQuery}`, 178 | headers: { 179 | cookie: "__ddgid_=; __ddg2_=; __ddg1_=", 180 | }, 181 | withCredentials: false, 182 | }); 183 | 184 | if (!response?.data?.length) return []; 185 | 186 | const searchResults: SearchResult[] = response.data.map((item: any) => { 187 | return { 188 | id: item.id.toString(), 189 | thumbnail: item.poster, 190 | title: item.title, 191 | }; 192 | }); 193 | 194 | return searchResults; 195 | }, 196 | async _totalSearch(media) { 197 | const titles = Array.from( 198 | new Set([media?.title?.english, media?.title?.romaji]) 199 | ); 200 | 201 | if (!titles?.length) return []; 202 | 203 | for (const title of titles) { 204 | try { 205 | const searchResults = await anime._search(title); 206 | 207 | if (!searchResults?.length) continue; 208 | 209 | return searchResults; 210 | } catch (err) { 211 | console.error(err); 212 | } 213 | } 214 | 215 | return []; 216 | }, 217 | _parseBetween(text, start, end) { 218 | let strArr = []; 219 | 220 | strArr = text.split(start); 221 | 222 | strArr = strArr[1].split(end); 223 | 224 | return strArr[0]; 225 | }, 226 | _packer: { 227 | detect: function (str) { 228 | return anime._packer.get_chunks(str).length > 0; 229 | }, 230 | 231 | get_chunks: function (str) { 232 | const chunks = str.match( 233 | /eval\(\(?function\(.*?(,0,\{\}\)\)|split\('\|'\)\)\))($|\n)/g 234 | ); 235 | return chunks ? chunks : []; 236 | }, 237 | 238 | unpack: function (str) { 239 | const chunks = anime._packer.get_chunks(str); 240 | 241 | for (let i = 0; i < chunks.length; i++) { 242 | const chunk = chunks[i].replace(/\n$/, ""); 243 | str = str.split(chunk).join(anime._packer.unpack_chunk(chunk)); 244 | } 245 | return str; 246 | }, 247 | 248 | unpack_chunk: function (str) { 249 | let unpacked_source = ""; 250 | const __eval = eval; 251 | if (anime._packer.detect(str)) { 252 | try { 253 | // @ts-expect-error Uh I have to disable 254 | eval = function (s) { 255 | // jshint ignore:line 256 | unpacked_source += s; 257 | return unpacked_source; 258 | }; // jshint ignore:line 259 | __eval(str); 260 | if (typeof unpacked_source === "string" && unpacked_source) { 261 | str = unpacked_source; 262 | } 263 | } catch (e) { 264 | // well, it failed. we'll just return the original, instead of crashing on user. 265 | } 266 | } 267 | 268 | // @ts-expect-error Uh I have to disable 269 | eval = __eval; // jshint ignore:line 270 | return str; 271 | }, 272 | }, 273 | }; 274 | -------------------------------------------------------------------------------- /src/modules/aniwatch/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Anime, 3 | SearchResult, 4 | Subtitle, 5 | VideoContainer, 6 | VideoServer, 7 | } from "../../types"; 8 | 9 | interface WindowAnime extends Anime { 10 | baseUrl: string; 11 | _totalSearch: (media: { 12 | id: number; 13 | title: { 14 | romaji: string; 15 | english: string; 16 | userPreferred: string; 17 | native: string; 18 | }; 19 | }) => Promise; 20 | _search: ( 21 | query: string, 22 | shouldRemoveDuplicates?: boolean 23 | ) => Promise; 24 | _servers: Record; 25 | } 26 | 27 | const anime: WindowAnime = { 28 | baseUrl: "https://aniwatch-api-72oo.onrender.com", 29 | 30 | getId: async ({ media }) => { 31 | const searchResults = await anime._totalSearch(media); 32 | 33 | sendResponse({ 34 | data: searchResults?.[0]?.id, 35 | }); 36 | }, 37 | 38 | getEpisodes: async ({ animeId }) => { 39 | const { data } = await sendRequest<{ 40 | episodes: { 41 | title: string; 42 | episodeId: string; 43 | number: number; 44 | isFiller: boolean; 45 | }[]; 46 | }>(`${anime.baseUrl}/anime/episodes/` + animeId); 47 | 48 | sendResponse( 49 | data?.episodes?.map((ep) => ({ 50 | id: ep.episodeId.replace("?ep=", "questionmarkep="), 51 | number: ep.number.toString(), 52 | title: ep.title, 53 | isFiller: ep.isFiller, 54 | })) 55 | ); 56 | }, 57 | 58 | search: async ({ query }) => { 59 | const searchResults = await anime._search(query); 60 | 61 | sendResponse(searchResults); 62 | }, 63 | 64 | loadVideoServers: async ({ episodeId }) => { 65 | const newEpisodeId = episodeId.replace("questionmarkep=", "?ep="); 66 | 67 | sendResponse([ 68 | { 69 | name: "Sub", 70 | embed: "", 71 | extraData: { 72 | id: newEpisodeId, 73 | category: "sub", 74 | }, 75 | }, 76 | { 77 | name: "Dub", 78 | embed: "", 79 | extraData: { 80 | id: newEpisodeId, 81 | category: "dub", 82 | }, 83 | }, 84 | { 85 | name: "Raw", 86 | embed: "", 87 | extraData: { 88 | id: newEpisodeId, 89 | category: "raw", 90 | }, 91 | }, 92 | ]); 93 | 94 | // const { data } = await sendRequest<{ 95 | // sub: { 96 | // serverName: string; 97 | // serverId: number; 98 | // }[]; 99 | // dub: { 100 | // serverName: string; 101 | // serverId: number; 102 | // }[]; 103 | // raw: { 104 | // serverName: string; 105 | // serverId: number; 106 | // }[]; 107 | // episodeId: string; 108 | // episodeNo: number; 109 | // }>(`${anime.baseUrl}/anime/servers?episodeId=` + newEpisodeId); 110 | 111 | // const subServers = data.sub.map((server) => { 112 | // const serverName = anime._servers[server.serverId] || "vidcloud"; 113 | 114 | // return { 115 | // name: `sub-${serverName}`, 116 | // embed: "", 117 | // extraData: { 118 | // id: newEpisodeId, 119 | // serverName: serverName.toString(), 120 | // category: "sub", 121 | // }, 122 | // }; 123 | // }); 124 | 125 | // const dubServers = data.dub.map((server) => { 126 | // const serverName = anime._servers[server.serverId] || "vidcloud"; 127 | 128 | // return { 129 | // name: `dub-${serverName}`, 130 | // embed: "", 131 | // extraData: { 132 | // id: newEpisodeId, 133 | // serverName: serverName.toString(), 134 | // category: "dub", 135 | // }, 136 | // }; 137 | // }); 138 | 139 | // const rawServers = data.raw.map((server) => { 140 | // const serverName = anime._servers[server.serverId] || "vidcloud"; 141 | 142 | // return { 143 | // name: `raw-${serverName}`, 144 | // embed: "", 145 | // extraData: { 146 | // id: newEpisodeId, 147 | // serverName: serverName.toString(), 148 | // category: "raw", 149 | // }, 150 | // }; 151 | // }); 152 | 153 | // sendResponse([...subServers, ...dubServers, ...rawServers]); 154 | }, 155 | 156 | async loadVideoContainer(videoServer: VideoServer) { 157 | const episodeId = videoServer.extraData?.id!; 158 | // const serverName = videoServer.extraData?.serverName!; 159 | const category = videoServer.extraData?.category!; 160 | 161 | // if (!episodeId || !serverName || !category) { 162 | // return sendResponse(null); 163 | // } 164 | 165 | // const { data } = await sendRequest<{ 166 | // tracks: { file: string; kind: string; label: string }[]; 167 | // intro: { start: number; end: number }; 168 | // outro: { start: number; end: number }; 169 | // sources: { url: string; type: string }[]; 170 | // anilistID: number; 171 | // malID: number; 172 | // }>( 173 | // `${anime.baseUrl}/anime/episode-srcs?id=${episodeId}&server=${serverName}&category=${category}` 174 | // ); 175 | 176 | const { data } = await sendRequest<{ 177 | tracks: { file: string; kind: string; label: string }[]; 178 | intro: { start: number; end: number }; 179 | outro: { start: number; end: number }; 180 | sources: { url: string; type: string }[]; 181 | anilistID: number; 182 | malID: number; 183 | }>( 184 | `${anime.baseUrl}/anime/episode-srcs?id=${episodeId}&category=${category}` 185 | ); 186 | 187 | const container: VideoContainer = { 188 | videos: [], 189 | subtitles: [], 190 | timestamps: [], 191 | }; 192 | 193 | const subtitles: Subtitle[] = data?.tracks 194 | ?.filter((track) => track.kind === "captions") 195 | .map((track) => ({ 196 | file: { url: track.file }, 197 | language: track.label, 198 | })); 199 | 200 | container.subtitles = subtitles; 201 | container.timestamps = []; 202 | 203 | if (data?.intro) { 204 | container.timestamps?.push({ 205 | type: "Intro", 206 | startTime: data.intro.start, 207 | endTime: data.intro.end, 208 | }); 209 | } 210 | 211 | if (data?.outro) { 212 | container.timestamps?.push({ 213 | type: "Outro", 214 | startTime: data.outro.start, 215 | endTime: data.outro.end, 216 | }); 217 | } 218 | 219 | const getOrigin = (url: string): string => { 220 | const match = url.match(/^(https?:\/\/[^/]+)/); 221 | return match ? match[1] : ""; 222 | }; 223 | 224 | if (Array.isArray(data?.sources)) { 225 | data?.sources?.forEach((source) => { 226 | const sourceOrigin = getOrigin(source.url); 227 | 228 | container.videos.push({ 229 | file: { url: source.url, headers: { Referer: sourceOrigin } }, 230 | format: source.type as any, 231 | }); 232 | }); 233 | 234 | // container.videos.push({ 235 | // file: { url: data?.sources?.[0]?.url }, 236 | // format: "hls", 237 | // }); 238 | } 239 | 240 | sendResponse(container); 241 | }, 242 | 243 | async _search(query: string): Promise { 244 | if (!query) return []; 245 | 246 | if (query === "null") return []; 247 | 248 | const { data } = await sendRequest<{ 249 | animes: { 250 | id: string; 251 | name: string; 252 | poster: string; 253 | duration: string; 254 | type: string; 255 | rating: string; 256 | episodes: { 257 | sub: number; 258 | dub: number; 259 | }[]; 260 | }[]; 261 | }>(`${anime.baseUrl}/anime/search?q=` + encodeURIComponent(query)); 262 | 263 | return data?.animes?.map((item) => ({ 264 | id: item.id, 265 | thumbnail: item.poster, 266 | title: item.name, 267 | })); 268 | }, 269 | 270 | async _totalSearch(media) { 271 | const titles = Array.from( 272 | new Set([media?.title?.english, media?.title?.romaji]) 273 | ); 274 | 275 | if (!titles?.length) return []; 276 | 277 | for (const title of titles) { 278 | try { 279 | const searchResults = await anime._search(title); 280 | 281 | if (!searchResults?.length) continue; 282 | 283 | return searchResults; 284 | } catch (err) { 285 | console.error(err); 286 | } 287 | } 288 | 289 | return []; 290 | }, 291 | _servers: { 292 | 4: "vidstreaming", 293 | 1: "vidcloud", 294 | 5: "streamsb", 295 | 3: "streamtape", 296 | }, 297 | }; 298 | -------------------------------------------------------------------------------- /src/modules/hentaiz/index.ts: -------------------------------------------------------------------------------- 1 | import { Anime, SearchResult } from "../../types"; 2 | 3 | interface WindowAnime extends Anime { 4 | baseUrl: string; 5 | _totalSearch: (media: { 6 | id: number; 7 | title: { 8 | romaji: string; 9 | english: string; 10 | userPreferred: string; 11 | native: string; 12 | }; 13 | }) => Promise; 14 | _search: ( 15 | query: string, 16 | shouldRemoveDuplicates?: boolean 17 | ) => Promise; 18 | _removeDuplicates: ( 19 | arr: T[], 20 | shouldRemove: (a: T, b: T) => boolean 21 | ) => T[]; 22 | _dropRight: (array: T[], n?: number) => T[]; 23 | _parseBetween: (str: string, start: string, end: string) => string; 24 | _replaceAll: (str: string, find: string, replace: string) => string; 25 | } 26 | 27 | const anime: WindowAnime = { 28 | baseUrl: "https://ihentai.day", 29 | getId: async ({ media }) => { 30 | const searchResults = await anime._totalSearch(media); 31 | 32 | sendResponse({ 33 | data: searchResults?.[0]?.id, 34 | extraData: { 35 | searchTitle: searchResults?.[0]?.extra?.searchTitle, 36 | }, 37 | }); 38 | }, 39 | getEpisodes: async ({ animeId, extraData }) => { 40 | if (!extraData?.searchTitle) { 41 | sendResponse([]); 42 | 43 | return; 44 | } 45 | 46 | const searchResults = await anime._search(extraData.searchTitle, false); 47 | 48 | const correctsearchResults = searchResults.filter( 49 | (searchResult) => searchResult.id === animeId 50 | ); 51 | 52 | const filteredSearchResults = correctsearchResults.filter( 53 | (searchResult) => searchResult.extra?.idWithEpisode 54 | ); 55 | 56 | sendResponse( 57 | filteredSearchResults.map((searchResult) => ({ 58 | id: searchResult.extra?.idWithEpisode!, 59 | number: anime._replaceAll( 60 | searchResult.extra!.idWithEpisode!.split("-").pop()!, 61 | "/", 62 | "" 63 | ), 64 | thumbnail: searchResult?.extra?.bigThumbnail, 65 | })) 66 | ); 67 | }, 68 | async _search(query, shouldRemoveDuplicates = true) { 69 | const list: any[] = []; 70 | const LIMIT = 120; 71 | const MAX_PAGE = 3; 72 | 73 | const requestData = async (page: number): Promise => { 74 | const encodedQuery = encodeURIComponent(query); 75 | 76 | const { data: json } = await sendRequest( 77 | `https://bunny-cdn.com/api/videos?fields%5B0%5D=title&fields%5B1%5D=slug&fields%5B2%5D=views&filters%5B%24or%5D%5B0%5D%5Btitle%5D%5B%24contains%5D=${encodedQuery}&filters%5B%24or%5D%5B1%5D%5BotherTitles%5D%5B%24contains%5D=${encodedQuery}&filters%5B%24or%5D%5B2%5D%5Bcategories%5D%5Bname%5D%5B%24contains%5D=${encodedQuery}&filters%5B%24or%5D%5B3%5D%5Btags%5D%5Bname%5D%5B%24contains%5D=${encodedQuery}&filters%5B%24or%5D%5B4%5D%5Bstudios%5D%5Bname%5D%5B%24contains%5D=${encodedQuery}&filters%5B%24or%5D%5B5%5D%5Bseries%5D%5Bname%5D%5B%24contains%5D=${encodedQuery}&filters%5B%24or%5D%5B6%5D%5Byear%5D%5Bname%5D%5B%24contains%5D=${encodedQuery}&sort%5B0%5D=publishedAt%3Adesc&sort%5B1%5D=slug%3Adesc&pagination%5Bpage%5D=${page}&pagination%5BpageSize%5D=${LIMIT}&populate%5B0%5D=thumbnail&populate%5B1%5D=bigThumbnail` 78 | ); 79 | 80 | const dataWithTypes = json as { 81 | data: Array<{ 82 | id: number; 83 | publishedAt: string; 84 | slug: string; 85 | title: string; 86 | views: number; 87 | thumbnail: { 88 | id: number; 89 | name: string; 90 | alternativeText: any; 91 | caption: any; 92 | width: number; 93 | height: number; 94 | formats: { 95 | thumbnail: { 96 | name: string; 97 | hash: string; 98 | ext: string; 99 | mime: string; 100 | path: any; 101 | width: number; 102 | height: number; 103 | size: number; 104 | sizeInBytes: number; 105 | url: string; 106 | }; 107 | }; 108 | hash: string; 109 | ext: string; 110 | mime: string; 111 | size: number; 112 | url: string; 113 | previewUrl: any; 114 | provider: string; 115 | provider_metadata: any; 116 | createdAt: string; 117 | updatedAt: string; 118 | }; 119 | bigThumbnail: { 120 | id: number; 121 | name: string; 122 | alternativeText: any; 123 | caption: any; 124 | width: number; 125 | height: number; 126 | formats: { 127 | thumbnail: { 128 | name: string; 129 | hash: string; 130 | ext: string; 131 | mime: string; 132 | path: any; 133 | width: number; 134 | height: number; 135 | size: number; 136 | sizeInBytes: number; 137 | url: string; 138 | }; 139 | }; 140 | hash: string; 141 | ext: string; 142 | mime: string; 143 | size: number; 144 | url: string; 145 | previewUrl: any; 146 | provider: string; 147 | provider_metadata: any; 148 | createdAt: string; 149 | updatedAt: string; 150 | }; 151 | }>; 152 | meta: { 153 | pagination: { 154 | page: number; 155 | pageSize: number; 156 | pageCount: number; 157 | total: number; 158 | }; 159 | }; 160 | }; 161 | 162 | let searchResults = dataWithTypes.data.map((video) => { 163 | return { 164 | id: anime 165 | ._dropRight(video.slug.replaceAll("/", "").split("-")) 166 | .join("-"), 167 | title: anime._dropRight(video.title.split(" ")).join(" "), 168 | thumbnail: `https://bunny-cdn.com${video.thumbnail.url}`, 169 | extra: { 170 | searchTitle: anime._dropRight(video.title.split(" ")).join(" "), 171 | bigThumbnail: `https://bunny-cdn.com${video.bigThumbnail.url}`, 172 | idWithEpisode: video.slug.replaceAll("/", ""), 173 | }, 174 | }; 175 | }); 176 | 177 | if (shouldRemoveDuplicates) { 178 | searchResults = anime._removeDuplicates( 179 | searchResults, 180 | (a: any, b: any) => a.id === b.id 181 | ); 182 | } 183 | 184 | list.push(...searchResults); 185 | 186 | if (page * LIMIT < json.count && page < MAX_PAGE) { 187 | return requestData(page + 1); 188 | } 189 | 190 | return list; 191 | }; 192 | 193 | await requestData(1); 194 | 195 | return list; 196 | }, 197 | search: async ({ query }) => { 198 | const searchResults = await anime._search(query); 199 | 200 | sendResponse(searchResults); 201 | }, 202 | async loadVideoServers({ episodeId }) { 203 | sendResponse([ 204 | { 205 | name: "Sonar", 206 | extraData: { 207 | episodeId, 208 | }, 209 | embed: "", 210 | }, 211 | ]); 212 | }, 213 | 214 | async loadVideoContainer({ extraData }) { 215 | if (!extraData?.episodeId) return; 216 | 217 | const { data: text } = await sendRequest({ 218 | url: `${anime.baseUrl}/watch/${extraData.episodeId}`, 219 | }); 220 | 221 | const iframeSrc = anime._parseBetween(text, '