From 655ab9c9874c1cfe3e5c5fefa8ea99c3d114f939 Mon Sep 17 00:00:00 2001 From: lenafx Date: Sat, 29 Nov 2025 17:41:30 +0100 Subject: [PATCH] changed some files to ts --- package-lock.json | 223 ++++++++++++++++++ package.json | 12 +- ...nime.controller.js => anime.controller.ts} | 50 ++-- .../{anime.routes.js => anime.routes.ts} | 7 +- .../{anime.service.js => anime.service.ts} | 59 +++-- ...ooks.controller.js => books.controller.ts} | 42 ++-- .../{books.routes.js => books.routes.ts} | 7 +- .../{books.service.js => books.service.ts} | 97 ++++---- .../{proxy.routes.js => proxy.routes.ts} | 13 +- .../{proxy.service.js => proxy.service.ts} | 40 ++-- src/types.ts | 204 ++++++++++++++++ .../{views.routes.js => views.routes.ts} | 25 +- tsconfig.json | 13 + 13 files changed, 624 insertions(+), 168 deletions(-) rename src/anime/{anime.controller.js => anime.controller.ts} (65%) rename src/anime/{anime.routes.js => anime.routes.ts} (70%) rename src/anime/{anime.service.js => anime.service.ts} (68%) rename src/books/{books.controller.js => books.controller.ts} (65%) rename src/books/{books.routes.js => books.routes.ts} (68%) rename src/books/{books.service.js => books.service.ts} (76%) rename src/shared/proxy/{proxy.routes.js => proxy.routes.ts} (77%) rename src/shared/proxy/{proxy.service.js => proxy.service.ts} (64%) create mode 100644 src/types.ts rename src/views/{views.routes.js => views.routes.ts} (59%) create mode 100644 tsconfig.json diff --git a/package-lock.json b/package-lock.json index 65c24ca..f087cbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,24 @@ "@fastify/static": "^8.3.0", "fastify": "^5.6.2", "sqlite3": "^5.1.7" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.3.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" } }, "node_modules/@fastify/accept-negotiator": { @@ -233,6 +251,34 @@ "node": ">=12" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -284,6 +330,44 @@ "node": ">= 6" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -297,6 +381,32 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -416,6 +526,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -694,6 +811,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -784,6 +908,16 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1432,6 +1566,13 @@ "node": "20 || >=22" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-fetch-happen": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", @@ -2679,6 +2820,50 @@ "node": ">=0.6" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2691,6 +2876,27 @@ "node": "*" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -2717,6 +2923,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2889,6 +3102,16 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } } } } diff --git a/package.json b/package.json index 5581fdf..32fc226 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,10 @@ "name": "waifu-board-(server)", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "build": "tsc", + "dev": "ts-node server.js" }, "keywords": [], "author": "", @@ -14,5 +15,10 @@ "@fastify/static": "^8.3.0", "fastify": "^5.6.2", "sqlite3": "^5.1.7" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "typescript": "^5.3.0", + "ts-node": "^10.9.0" } -} +} \ No newline at end of file diff --git a/src/anime/anime.controller.js b/src/anime/anime.controller.ts similarity index 65% rename from src/anime/anime.controller.js rename to src/anime/anime.controller.ts index f8ed23d..ee44027 100644 --- a/src/anime/anime.controller.js +++ b/src/anime/anime.controller.ts @@ -1,19 +1,25 @@ -const animeService = require('./anime.service'); -const { getExtension, getExtensionsList } = require('../shared/extensions'); +import {FastifyReply, FastifyRequest} from 'fastify'; +import * as animeService from './anime.service'; +import { getExtension, getExtensionsList } from '../shared/extensions'; +import {AnimeRequest, SearchRequest, ExtensionNameRequest, WatchStreamRequest, Anime} from '../types'; -async function getAnime(req, reply) { +export async function getAnime(req: AnimeRequest, reply: FastifyReply) { try { const { id } = req.params; const source = req.query.ext || 'anilist'; - let anime; + let anime: Anime | { error: string }; if (source === 'anilist') { anime = await animeService.getAnimeById(id); } else { const extensionName = source; const ext = getExtension(extensionName); - const results = await animeService.searchAnimeInExtension(ext, extensionName, id.replaceAll("-", " ")); + const results = await animeService.searchAnimeInExtension( + ext, + extensionName, + id.replaceAll("-", " ") + ); anime = results[0] || null; } @@ -23,7 +29,7 @@ async function getAnime(req, reply) { } } -async function getTrending(req, reply) { +export async function getTrending(req: FastifyRequest, reply: FastifyReply) { try { const results = await animeService.getTrendingAnime(); return { results }; @@ -32,7 +38,7 @@ async function getTrending(req, reply) { } } -async function getTopAiring(req, reply) { +export async function getTopAiring(req: FastifyRequest, reply: FastifyReply) { try { const results = await animeService.getTopAiringAnime(); return { results }; @@ -41,7 +47,7 @@ async function getTopAiring(req, reply) { } } -async function search(req, reply) { +export async function search(req: SearchRequest, reply: FastifyReply) { try { const query = req.query.q; const results = await animeService.searchAnimeLocal(query); @@ -58,11 +64,11 @@ async function search(req, reply) { } } -async function getExtensions(req, reply) { +export async function getExtensions(req: FastifyRequest, reply: FastifyReply) { return { extensions: getExtensionsList() }; } -async function getExtensionSettings(req, reply) { +export async function getExtensionSettings(req: ExtensionNameRequest, reply: FastifyReply) { const { name } = req.params; const ext = getExtension(name); @@ -77,19 +83,18 @@ async function getExtensionSettings(req, reply) { return ext.getSettings(); } -async function getWatchStream(req, reply) { +export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) { try { const { animeId, episode, server, category, ext } = req.query; const extension = getExtension(ext); if (!extension) return { error: "Extension not found" }; - let anime; + let anime: Anime | { error: string }; if (!isNaN(Number(animeId))) { anime = await animeService.getAnimeById(animeId); - if (anime.error) return { error: "Anime metadata not found" }; - } - else { + if ('error' in anime) return { error: "Anime metadata not found" }; + } else { const results = await animeService.searchAnimeInExtension( extension, ext, @@ -107,16 +112,7 @@ async function getWatchStream(req, reply) { category ); } catch (err) { - return { error: err.message }; + const error = err as Error; + return { error: error.message }; } -} - -module.exports = { - getAnime, - getTrending, - getTopAiring, - search, - getExtensions, - getExtensionSettings, - getWatchStream -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/anime/anime.routes.js b/src/anime/anime.routes.ts similarity index 70% rename from src/anime/anime.routes.js rename to src/anime/anime.routes.ts index 1c2c712..4d9c632 100644 --- a/src/anime/anime.routes.js +++ b/src/anime/anime.routes.ts @@ -1,6 +1,7 @@ -const controller = require('./anime.controller'); +import { FastifyInstance } from 'fastify'; +import * as controller from './anime.controller'; -async function animeRoutes(fastify, options) { +async function animeRoutes(fastify: FastifyInstance) { fastify.get('/anime/:id', controller.getAnime); fastify.get('/trending', controller.getTrending); fastify.get('/top-airing', controller.getTopAiring); @@ -10,4 +11,4 @@ async function animeRoutes(fastify, options) { fastify.get('/watch/stream', controller.getWatchStream); } -module.exports = animeRoutes; \ No newline at end of file +export default animeRoutes; \ No newline at end of file diff --git a/src/anime/anime.service.js b/src/anime/anime.service.ts similarity index 68% rename from src/anime/anime.service.js rename to src/anime/anime.service.ts index 2d4f2ab..9c40de3 100644 --- a/src/anime/anime.service.js +++ b/src/anime/anime.service.ts @@ -1,7 +1,8 @@ -const { queryOne, queryAll } = require('../shared/database'); -const {getAllExtensions} = require("../shared/extensions"); +import { queryOne, queryAll } from '../shared/database'; +import { getAllExtensions } from '../shared/extensions'; +import { Anime, Extension, StreamData } from '../types'; -async function getAnimeById(id) { +export async function getAnimeById(id: string | number): Promise { const row = await queryOne("SELECT full_data FROM anime WHERE id = ?", [id]); if (!row) { @@ -11,17 +12,17 @@ async function getAnimeById(id) { return JSON.parse(row.full_data); } -async function getTrendingAnime() { +export async function getTrendingAnime(): Promise { const rows = await queryAll("SELECT full_data FROM trending ORDER BY rank ASC LIMIT 10"); - return rows.map(r => JSON.parse(r.full_data)); + return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data)); } -async function getTopAiringAnime() { +export async function getTopAiringAnime(): Promise { const rows = await queryAll("SELECT full_data FROM top_airing ORDER BY rank ASC LIMIT 10"); - return rows.map(r => JSON.parse(r.full_data)); + return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data)); } -async function searchAnimeLocal(query) { +export async function searchAnimeLocal(query: string): Promise { if (!query || query.length < 2) { return []; } @@ -29,7 +30,7 @@ async function searchAnimeLocal(query) { const sql = `SELECT full_data FROM anime WHERE full_data LIKE ? LIMIT 50`; const rows = await queryAll(sql, [`%${query}%`]); - const results = rows.map(row => JSON.parse(row.full_data)); + const results: Anime[] = rows.map((row: { full_data: string; }) => JSON.parse(row.full_data)); const cleanResults = results.filter(anime => { const q = query.toLowerCase(); @@ -38,7 +39,7 @@ async function searchAnimeLocal(query) { anime.title.romaji, anime.title.native, ...(anime.synonyms || []) - ].filter(Boolean).map(t => t.toLowerCase()); + ].filter(Boolean).map(t => t!.toLowerCase()); return titles.some(t => t.includes(q)); }); @@ -46,7 +47,13 @@ async function searchAnimeLocal(query) { return cleanResults.slice(0, 10); } -async function searchAnimeInExtension(ext, name, query) { +export async function searchAnimeInExtension( + ext: Extension | null, + name: string, + query: string +): Promise { + if (!ext) return []; + if ((ext.type === 'anime-board') && ext.search) { try { console.log(`[${name}] Searching for book: ${query}`); @@ -63,7 +70,7 @@ async function searchAnimeInExtension(ext, name, query) { return matches.map(m => ({ id: m.id, extensionName: name, - title: { romaji: m.title, english: m.title }, + title: { romaji: m.title, english: m.title, native: null }, coverImage: { large: m.image || '' }, averageScore: m.rating || m.score || null, format: 'ANIME', @@ -75,9 +82,11 @@ async function searchAnimeInExtension(ext, name, query) { console.error(`Extension search failed for ${name}:`, e); } } + + return []; } -async function searchAnimeExtensions(query) { +export async function searchAnimeExtensions(query: string): Promise { const extensions = getAllExtensions(); for (const [name, ext] of extensions) { @@ -88,7 +97,13 @@ async function searchAnimeExtensions(query) { return []; } -async function getStreamData(extension, animeData, episode, server, category) { +export async function getStreamData( + extension: Extension, + animeData: Anime, + episode: string, + server?: string, + category?: string +): Promise { const searchOptions = { query: animeData.title.english || animeData.title.romaji, dub: category === 'dub', @@ -99,6 +114,10 @@ async function getStreamData(extension, animeData, episode, server, category) { } }; + if (!extension.search || !extension.findEpisodes || !extension.findEpisodeServer) { + throw new Error("Extension doesn't support required methods"); + } + const searchResults = await extension.search(searchOptions); if (!searchResults || searchResults.length === 0) { @@ -117,14 +136,4 @@ async function getStreamData(extension, animeData, episode, server, category) { const streamData = await extension.findEpisodeServer(targetEp, serverName); return streamData; -} - -module.exports = { - getAnimeById, - getTrendingAnime, - getTopAiringAnime, - searchAnimeLocal, - searchAnimeExtensions, - searchAnimeInExtension, - getStreamData -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/books/books.controller.js b/src/books/books.controller.ts similarity index 65% rename from src/books/books.controller.js rename to src/books/books.controller.ts index e969252..32dae8f 100644 --- a/src/books/books.controller.js +++ b/src/books/books.controller.ts @@ -1,14 +1,16 @@ -const booksService = require('./books.service'); -const {getExtension} = require("../shared/extensions"); +import {FastifyReply, FastifyRequest} from 'fastify'; +import * as booksService from './books.service'; +import { getExtension } from '../shared/extensions'; +import { BookRequest, SearchRequest, ChapterRequest } from '../types'; -async function getBook(req, reply) { +export async function getBook(req: BookRequest, reply: FastifyReply) { try { const { id } = req.params; const source = req.query.ext || 'anilist'; let book; if (source === 'anilist') { - book = await booksService.getBookById(id); + book = await booksService.getBookById(id); } else { const extensionName = source; const ext = getExtension(extensionName); @@ -20,11 +22,12 @@ async function getBook(req, reply) { return book; } catch (err) { - return { error: err.toString() }; + const error = err as Error; + return { error: error.toString() }; } } -async function getTrending(req, reply) { +export async function getTrending(req: FastifyRequest, reply: FastifyReply) { try { const results = await booksService.getTrendingBooks(); return { results }; @@ -33,7 +36,7 @@ async function getTrending(req, reply) { } } -async function getPopular(req, reply) { +export async function getPopular(req: FastifyRequest, reply: FastifyReply) { try { const results = await booksService.getPopularBooks(); return { results }; @@ -42,7 +45,7 @@ async function getPopular(req, reply) { } } -async function searchBooks(req, reply) { +export async function searchBooks(req: SearchRequest, reply: FastifyReply) { try { const query = req.query.q; @@ -60,13 +63,14 @@ async function searchBooks(req, reply) { const extResults = await booksService.searchBooksExtensions(query); return { results: extResults }; - } catch(e) { - console.error("Search Error:", e.message); + } catch (e) { + const error = e as Error; + console.error("Search Error:", error.message); return { results: [] }; } } -async function getChapters(req, reply) { +export async function getChapters(req: BookRequest, reply: FastifyReply) { try { const { id } = req.params; return await booksService.getChaptersForBook(id); @@ -75,7 +79,7 @@ async function getChapters(req, reply) { } } -async function getChapterContent(req, reply) { +export async function getChapterContent(req: ChapterRequest, reply: FastifyReply) { try { const { bookId, chapter, provider } = req.params; @@ -87,17 +91,9 @@ async function getChapterContent(req, reply) { return reply.send(content); } catch (err) { - console.error("getChapterContent error:", err.message); + const error = err as Error; + console.error("getChapterContent error:", error.message); return reply.code(500).send({ error: "Error loading chapter" }); } -} - -module.exports = { - getBook, - getTrending, - getPopular, - searchBooks, - getChapters, - getChapterContent -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/books/books.routes.js b/src/books/books.routes.ts similarity index 68% rename from src/books/books.routes.js rename to src/books/books.routes.ts index bc9db00..6ab887c 100644 --- a/src/books/books.routes.js +++ b/src/books/books.routes.ts @@ -1,6 +1,7 @@ -const controller = require('./books.controller'); +import { FastifyInstance } from 'fastify'; +import * as controller from './books.controller'; -async function booksRoutes(fastify, options) { +async function booksRoutes(fastify: FastifyInstance) { fastify.get('/book/:id', controller.getBook); fastify.get('/books/trending', controller.getTrending); fastify.get('/books/popular', controller.getPopular); @@ -9,4 +10,4 @@ async function booksRoutes(fastify, options) { fastify.get('/book/:bookId/:chapter/:provider', controller.getChapterContent); } -module.exports = booksRoutes; \ No newline at end of file +export default booksRoutes; \ No newline at end of file diff --git a/src/books/books.service.js b/src/books/books.service.ts similarity index 76% rename from src/books/books.service.js rename to src/books/books.service.ts index 3c7a1a3..605b9dd 100644 --- a/src/books/books.service.js +++ b/src/books/books.service.ts @@ -1,7 +1,8 @@ -const { queryOne, queryAll } = require('../shared/database'); -const { getAllExtensions } = require('../shared/extensions'); +import { queryOne, queryAll } from '../shared/database'; +import { getAllExtensions } from '../shared/extensions'; +import { Book, Extension, ChapterWithProvider, ChapterContent } from '../types'; -async function getBookById(id) { +export async function getBookById(id: string | number): Promise { const row = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]); if (row) { @@ -31,31 +32,31 @@ async function getBookById(id) { const response = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, - body: JSON.stringify({ query, variables: { id: parseInt(id) } }) + body: JSON.stringify({ query, variables: { id: parseInt(id.toString()) } }) }); const data = await response.json(); if (data.data && data.data.Media) { return data.data.Media; } - } catch(e) { + } catch (e) { console.error("Fetch error:", e); } return { error: "Book not found" }; } -async function getTrendingBooks() { +export async function getTrendingBooks(): Promise { const rows = await queryAll("SELECT full_data FROM trending_books ORDER BY rank ASC LIMIT 10"); - return rows.map(r => JSON.parse(r.full_data)); + return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data)); } -async function getPopularBooks() { +export async function getPopularBooks(): Promise { const rows = await queryAll("SELECT full_data FROM popular_books ORDER BY rank ASC LIMIT 10"); - return rows.map(r => JSON.parse(r.full_data)); + return rows.map((r: { full_data: string; }) => JSON.parse(r.full_data)); } -async function searchBooksLocal(query) { +export async function searchBooksLocal(query: string): Promise { if (!query || query.length < 2) { return []; } @@ -63,7 +64,7 @@ async function searchBooksLocal(query) { const sql = `SELECT full_data FROM books WHERE full_data LIKE ? LIMIT 50`; const rows = await queryAll(sql, [`%${query}%`]); - const results = rows.map(row => JSON.parse(row.full_data)); + const results: Book[] = rows.map((row: { full_data: string; }) => JSON.parse(row.full_data)); const clean = results.filter(book => { const searchTerms = [ @@ -71,7 +72,7 @@ async function searchBooksLocal(query) { book.title.romaji, book.title.native, ...(book.synonyms || []) - ].filter(Boolean).map(t => t.toLowerCase()); + ].filter(Boolean).map(t => t!.toLowerCase()); return searchTerms.some(term => term.includes(query.toLowerCase())); }); @@ -79,7 +80,7 @@ async function searchBooksLocal(query) { return clean.slice(0, 10); } -async function searchBooksAniList(query) { +export async function searchBooksAniList(query: string): Promise { const gql = ` query ($search: String) { Page(page: 1, perPage: 5) { @@ -107,7 +108,7 @@ async function searchBooksAniList(query) { return []; } -async function searchBooksInExtension(ext, name, query) { +export async function searchBooksInExtension(ext: Extension | null, name: string, query: string): Promise { if (!ext) return []; if ((ext.type === 'book-board' || ext.type === 'manga-board') && ext.search) { @@ -126,7 +127,7 @@ async function searchBooksInExtension(ext, name, query) { return matches.map(m => ({ id: m.id, extensionName: name, - title: { romaji: m.title, english: m.title }, + title: { romaji: m.title, english: m.title, native: null }, coverImage: { large: m.image || '' }, averageScore: m.rating || m.score || null, format: 'MANGA', @@ -142,7 +143,7 @@ async function searchBooksInExtension(ext, name, query) { return []; } -async function searchBooksExtensions(query) { +export async function searchBooksExtensions(query: string): Promise { const extensions = getAllExtensions(); for (const [name, ext] of extensions) { @@ -153,16 +154,17 @@ async function searchBooksExtensions(query) { return []; } -async function getChaptersForBook(id) { - let bookData = null; - let searchTitle = null; +export async function getChaptersForBook(id: string): Promise<{ chapters: ChapterWithProvider[] }> { + let bookData: Book | null = null; + let searchTitle: string | null = null; if (typeof id === "string" && isNaN(Number(id))) { searchTitle = id.replaceAll("-", " "); } else { - bookData = await queryOne("SELECT full_data FROM books WHERE id = ?", [id]) - .then(row => row ? JSON.parse(row.full_data) : null) - .catch(() => null); + const result = await getBookById(id); + if (!('error' in result)) { + bookData = result; + } if (!bookData) { try { @@ -181,17 +183,16 @@ async function getChaptersForBook(id) { const d = await res.json(); if (d.data?.Media) bookData = d.data.Media; - } catch (e) {} + } catch (e) { } } if (!bookData) return { chapters: [] }; - const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean); + const titles = [bookData.title.english, bookData.title.romaji].filter(Boolean) as string[]; searchTitle = titles[0]; } - - const allChapters = []; + const allChapters: ChapterWithProvider[] = []; const extensions = getAllExtensions(); const searchPromises = Array.from(extensions.entries()) @@ -203,25 +204,25 @@ async function getChaptersForBook(id) { try { console.log(`[${name}] Searching chapters for: ${searchTitle}`); - const matches = await ext.search({ - query: searchTitle, + const matches = await ext.search!({ + query: searchTitle!, media: bookData ? { romajiTitle: bookData.title.romaji, - englishTitle: bookData.title.english, - startDate: bookData.startDate - } : {} + englishTitle: bookData.title.english || "", + startDate: bookData.startDate || { year: 0, month: 0, day: 0 } + } : { romajiTitle: searchTitle!, englishTitle: searchTitle!, startDate: { year: 0, month: 0, day: 0 } } }); if (matches?.length) { const best = matches[0]; - const chaps = await ext.findChapters(best.id); + const chaps = await ext.findChapters!(best.id); if (chaps?.length) { console.log(`[${name}] Found ${chaps.length} chapters.`); - chaps.forEach(ch => { + chaps.forEach((ch: { id: any; number: { toString: () => string; }; title: any; releaseDate: any; }) => { allChapters.push({ id: ch.id, - number: parseFloat(ch.number), + number: parseFloat(ch.number.toString()), title: ch.title, date: ch.releaseDate, provider: name @@ -232,16 +233,17 @@ async function getChaptersForBook(id) { console.log(`[${name}] No matches found for book.`); } } catch (e) { - console.error(`Failed to fetch chapters from ${name}:`, e.message); + const error = e as Error; + console.error(`Failed to fetch chapters from ${name}:`, error.message); } }); await Promise.all(searchPromises); - return { chapters: allChapters.sort((a, b) => a.number - b.number) }; + return { chapters: allChapters.sort((a, b) => Number(a.number) - Number(b.number)) }; } -async function getChapterContent(bookId, chapterIndex, providerName) { +export async function getChapterContent(bookId: string, chapterIndex: string, providerName: string): Promise { const extensions = getAllExtensions(); const ext = extensions.get(providerName); @@ -273,6 +275,10 @@ async function getChapterContent(bookId, chapterIndex, providerName) { const chapterNumber = typeof selectedChapter.number === 'number' ? selectedChapter.number : index; try { + if (!ext.findChapterPages) { + throw new Error("Extension doesn't support findChapterPages"); + } + if (ext.mediaType === "manga") { const pages = await ext.findChapterPages(chapterId); return { @@ -299,19 +305,8 @@ async function getChapterContent(bookId, chapterIndex, providerName) { throw new Error("Unknown mediaType"); } catch (err) { - console.error(`[Chapter] Error loading from ${providerName}:`, err && err.message ? err.message : err); + const error = err as Error; + console.error(`[Chapter] Error loading from ${providerName}:`, error.message); throw err; } -} - -module.exports = { - getBookById, - getTrendingBooks, - getPopularBooks, - searchBooksLocal, - searchBooksAniList, - searchBooksExtensions, - searchBooksInExtension, - getChaptersForBook, - getChapterContent -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/shared/proxy/proxy.routes.js b/src/shared/proxy/proxy.routes.ts similarity index 77% rename from src/shared/proxy/proxy.routes.js rename to src/shared/proxy/proxy.routes.ts index 2a4d434..259ae6b 100644 --- a/src/shared/proxy/proxy.routes.js +++ b/src/shared/proxy/proxy.routes.ts @@ -1,8 +1,9 @@ -const { proxyRequest, processM3U8Content, streamToReadable } = require('./proxy.service'); +import { FastifyInstance, FastifyReply } from 'fastify'; +import { proxyRequest, processM3U8Content, streamToReadable } from './proxy.service'; +import { ProxyRequest } from '../../types'; -async function proxyRoutes(fastify, options) { - - fastify.get('/proxy', async (req, reply) => { +async function proxyRoutes(fastify: FastifyInstance) { + fastify.get('/proxy', async (req: ProxyRequest, reply: FastifyReply) => { const { url, referer, origin, userAgent } = req.query; if (!url) { @@ -34,7 +35,7 @@ async function proxyRoutes(fastify, options) { return processedContent; } else { - return reply.send(streamToReadable(response.body)); + return reply.send(streamToReadable(response.body!)); } } catch (err) { @@ -44,4 +45,4 @@ async function proxyRoutes(fastify, options) { }); } -module.exports = proxyRoutes; \ No newline at end of file +export default proxyRoutes; \ No newline at end of file diff --git a/src/shared/proxy/proxy.service.js b/src/shared/proxy/proxy.service.ts similarity index 64% rename from src/shared/proxy/proxy.service.js rename to src/shared/proxy/proxy.service.ts index 7b1e1da..7552c5f 100644 --- a/src/shared/proxy/proxy.service.js +++ b/src/shared/proxy/proxy.service.ts @@ -1,7 +1,19 @@ -const { Readable } = require('stream'); +import { Readable } from 'stream'; -async function proxyRequest(url, { referer, origin, userAgent }) { - const headers = { +interface ProxyHeaders { + referer?: string; + origin?: string; + userAgent?: string; +} + +interface ProxyResponse { + response: Response; + contentType: string | null; + isM3U8: boolean; +} + +export async function proxyRequest(url: string, { referer, origin, userAgent }: ProxyHeaders): Promise { + const headers: Record = { 'User-Agent': userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", 'Accept': '*/*', 'Accept-Language': 'en-US,en;q=0.9' @@ -26,14 +38,18 @@ async function proxyRequest(url, { referer, origin, userAgent }) { }; } -function processM3U8Content(text, baseUrl, { referer, origin, userAgent }) { +export function processM3U8Content( + text: string, + baseUrl: URL, + { referer, origin, userAgent }: ProxyHeaders +): string { return text.replace(/^(?!#)(?!\s*$).+/gm, (line) => { line = line.trim(); - let absoluteUrl; + let absoluteUrl: string; try { absoluteUrl = new URL(line, baseUrl).href; - } catch(e) { + } catch (e) { return line; } @@ -47,12 +63,6 @@ function processM3U8Content(text, baseUrl, { referer, origin, userAgent }) { }); } -function streamToReadable(webStream) { - return Readable.fromWeb(webStream); -} - -module.exports = { - proxyRequest, - processM3U8Content, - streamToReadable -}; \ No newline at end of file +export function streamToReadable(webStream: ReadableStream): Readable { + return Readable.fromWeb(webStream as any); +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b0751ce --- /dev/null +++ b/src/types.ts @@ -0,0 +1,204 @@ +import { FastifyRequest, FastifyReply } from 'fastify'; + +export interface AnimeTitle { + romaji: string; + english: string | null; + native: string | null; + userPreferred?: string; +} + +export interface CoverImage { + extraLarge?: string; + large: string; + medium?: string; + color?: string; +} + +export interface StartDate { + year: number; + month: number; + day: number; +} + +export interface Anime { + id: number | string; + title: AnimeTitle; + coverImage: CoverImage; + bannerImage?: string; + description?: string; + averageScore: number | null; + format: string; + seasonYear: number | null; + startDate?: StartDate; + synonyms?: string[]; + extensionName?: string; + isExtensionResult?: boolean; +} + +export interface Book { + id: number | string; + title: AnimeTitle; + coverImage: CoverImage; + bannerImage?: string; + description?: string; + averageScore: number | null; + format: string; + seasonYear: number | null; + startDate?: StartDate; + synonyms?: string[]; + extensionName?: string; + isExtensionResult?: boolean; +} + +export interface ExtensionSearchOptions { + query: string; + dub?: boolean; + media?: { + romajiTitle: string; + englishTitle: string; + startDate: StartDate; + }; +} + +export interface ExtensionSearchResult { + id: string; + title: string; + image?: string; + rating?: number; + score?: number; +} + +export interface Episode { + id: string; + number: number; + title?: string; +} + +export interface Chapter { + id: string; + number: string | number; + title?: string; + releaseDate?: string; +} + +export interface ChapterWithProvider extends Chapter { + provider: string; + date?: string; +} + +export interface Extension { + type: 'anime-board' | 'book-board' | 'manga-board'; + mediaType?: 'manga' | 'ln'; + search?: (options: ExtensionSearchOptions) => Promise; + findEpisodes?: (id: string) => Promise; + findEpisodeServer?: (episode: Episode, server: string) => Promise; + findChapters?: (id: string) => Promise; + findChapterPages?: (chapterId: string) => Promise; + getSettings?: () => ExtensionSettings; +} + +export interface ExtensionSettings { + episodeServers: string[]; + supportsDub: boolean; +} + +export interface StreamData { + url?: string; + sources?: any[]; + subtitles?: any[]; +} + +export interface MangaChapterContent { + type: 'manga'; + chapterId: string; + title: string | null; + number: number; + provider: string; + pages: any[]; +} + +export interface LightNovelChapterContent { + type: 'ln'; + chapterId: string; + title: string | null; + number: number; + provider: string; + content: any; +} + +export type ChapterContent = MangaChapterContent | LightNovelChapterContent; + +export interface AnimeParams { + id: string; +} + +export interface AnimeQuery { + ext?: string; +} + +export interface SearchQuery { + q: string; +} + +export interface ExtensionNameParams { + name: string; +} + +export interface WatchStreamQuery { + animeId: string; + episode: string; + server?: string; + category?: string; + ext: string; +} + +export interface BookParams { + id: string; +} + +export interface BookQuery { + ext?: string; +} + +export interface ChapterParams { + bookId: string; + chapter: string; + provider: string; +} + +export interface ProxyQuery { + url: string; + referer?: string; + origin?: string; + userAgent?: string; +} + +export type AnimeRequest = FastifyRequest<{ + Params: AnimeParams; + Querystring: AnimeQuery; +}>; + +export type SearchRequest = FastifyRequest<{ + Querystring: SearchQuery; +}>; + +export type ExtensionNameRequest = FastifyRequest<{ + Params: ExtensionNameParams; +}>; + +export type WatchStreamRequest = FastifyRequest<{ + Querystring: WatchStreamQuery; +}>; + +export type BookRequest = FastifyRequest<{ + Params: BookParams; + Querystring: BookQuery; +}>; + +export type ChapterRequest = FastifyRequest<{ + Params: ChapterParams; +}>; + +export type ProxyRequest = FastifyRequest<{ + Querystring: ProxyQuery; +}>; \ No newline at end of file diff --git a/src/views/views.routes.js b/src/views/views.routes.ts similarity index 59% rename from src/views/views.routes.js rename to src/views/views.routes.ts index 9981ace..7b34e2e 100644 --- a/src/views/views.routes.js +++ b/src/views/views.routes.ts @@ -1,47 +1,48 @@ -const fs = require('fs'); -const path = require('path'); +import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; +import * as fs from 'fs'; +import * as path from 'path'; -async function viewsRoutes(fastify, options) { +async function viewsRoutes(fastify: FastifyInstance) { - fastify.get('/', (req, reply) => { + fastify.get('/', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'index.html')); reply.type('text/html').send(stream); }); - fastify.get('/books', (req, reply) => { + fastify.get('/books', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'books.html')); reply.type('text/html').send(stream); }); - fastify.get('/anime/:id', (req, reply) => { + fastify.get('/anime/:id', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'anime.html')); reply.type('text/html').send(stream); }); - fastify.get('/anime/:extension/*', (req, reply) => { + fastify.get('/anime/:extension/*', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'anime.html')); reply.type('text/html').send(stream); }); - fastify.get('/watch/:id/:episode', (req, reply) => { + fastify.get('/watch/:id/:episode', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'watch.html')); reply.type('text/html').send(stream); }); - fastify.get('/book/:id', (req, reply) => { + fastify.get('/book/:id', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'book.html')); reply.type('text/html').send(stream); }); - fastify.get('/book/:extension/*', (req, reply) => { + fastify.get('/book/:extension/*', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'book.html')); reply.type('text/html').send(stream); }); - fastify.get('/read/:provider/:chapter/*', (req, reply) => { + fastify.get('/read/:provider/:chapter/*', (req: FastifyRequest, reply: FastifyReply) => { const stream = fs.createReadStream(path.join(__dirname, '..', '..', 'views', 'read.html')); reply.type('text/html').send(stream); }); } -module.exports = viewsRoutes; \ No newline at end of file +export default viewsRoutes; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..93e85a1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "CommonJS", + "allowJs": true, + "checkJs": false, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "dist" + }, + "include": ["src/**/*"] +}