diff options
| author | Ritesh Ghosh <[email protected]> | 2024-12-07 21:17:27 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-12-07 21:17:27 +0530 |
| commit | b0718180a0c314a6bab262a673f591a3f1b40033 (patch) | |
| tree | e37f64919d75cb243d70dd9733a0142180d860b4 /src | |
| parent | 03771a209685194ee75c188c93d381c51b839810 (diff) | |
| parent | 02c8d107dc96c6795615778171ba82e2b50a3e10 (diff) | |
| download | aniwatch-api-b0718180a0c314a6bab262a673f591a3f1b40033.tar.xz aniwatch-api-b0718180a0c314a6bab262a673f591a3f1b40033.zip | |
Merge pull request #80 from ghoshRitesh12/cache
Add caching layer for improved performance
Diffstat (limited to 'src')
| -rw-r--r-- | src/config/cache.ts | 50 | ||||
| -rw-r--r-- | src/config/variables.ts | 8 | ||||
| -rw-r--r-- | src/routes/hianime.ts | 97 | ||||
| -rw-r--r-- | src/server.ts | 24 |
4 files changed, 158 insertions, 21 deletions
diff --git a/src/config/cache.ts b/src/config/cache.ts new file mode 100644 index 0000000..6953fbc --- /dev/null +++ b/src/config/cache.ts @@ -0,0 +1,50 @@ +import { config } from "dotenv"; +import { Redis } from "ioredis"; + +config(); + +export class AniwatchAPICache { + private _client: Redis | null; + public isOptional: boolean = true; + + static DEFAULT_CACHE_EXPIRY_SECONDS = 60 as const; + static CACHE_EXPIRY_HEADER_NAME = "X-ANIWATCH-CACHE-EXPIRY" as const; + + constructor() { + const redisConnURL = process.env?.ANIWATCH_API_REDIS_CONN_URL; + this.isOptional = !Boolean(redisConnURL); + this._client = this.isOptional ? null : new Redis(String(redisConnURL)); + } + + set(key: string | Buffer, value: string | Buffer | number) { + if (this.isOptional) return; + return this._client?.set(key, value); + } + + get(key: string | Buffer) { + if (this.isOptional) return; + return this._client?.get(key); + } + + /** + * @param expirySeconds set to 60 by default + */ + async getOrSet<T>( + key: string | Buffer, + setCB: () => Promise<T>, + expirySeconds: number = AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS + ) { + const cachedData = this.isOptional + ? null + : (await this._client?.get(key)) || null; + let data = JSON.parse(String(cachedData)) as T; + + if (!data) { + data = await setCB(); + await this._client?.set(key, JSON.stringify(data), "EX", expirySeconds); + } + return data; + } +} + +export const cache = new AniwatchAPICache(); diff --git a/src/config/variables.ts b/src/config/variables.ts new file mode 100644 index 0000000..b58316f --- /dev/null +++ b/src/config/variables.ts @@ -0,0 +1,8 @@ +type CacheVariables = { + CACHE_CONFIG: { + key: string; + duration: number; + }; +}; + +export type AniwatchAPIVariables = {} & CacheVariables; diff --git a/src/routes/hianime.ts b/src/routes/hianime.ts index f4e1ff9..fd34e5d 100644 --- a/src/routes/hianime.ts +++ b/src/routes/hianime.ts @@ -1,123 +1,186 @@ import { Hono } from "hono"; import { HiAnime } from "aniwatch"; +import { cache } from "../config/cache.js"; +import type { AniwatchAPIVariables } from "../config/variables.js"; const hianime = new HiAnime.Scraper(); -const hianimeRouter = new Hono(); +const hianimeRouter = new Hono<{ Variables: AniwatchAPIVariables }>(); // /api/v2/hianime hianimeRouter.get("/", (c) => c.redirect("/", 301)); // /api/v2/hianime/home hianimeRouter.get("/home", async (c) => { - const data = await hianime.getHomePage(); + const cacheConfig = c.get("CACHE_CONFIG"); + + const data = await cache.getOrSet<HiAnime.ScrapedHomePage>( + cacheConfig.key, + hianime.getHomePage, + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/category/{name}?page={page} hianimeRouter.get("/category/:name", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const categoryName = decodeURIComponent( c.req.param("name").trim() ) as HiAnime.AnimeCategories; - const page: number = Number(decodeURIComponent(c.req.query("page") || "")) || 1; - const data = await hianime.getCategoryAnime(categoryName, page); + const data = await cache.getOrSet<HiAnime.ScrapedAnimeCategory>( + cacheConfig.key, + async () => hianime.getCategoryAnime(categoryName, page), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/genre/{name}?page={page} hianimeRouter.get("/genre/:name", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const genreName = decodeURIComponent(c.req.param("name").trim()); const page: number = Number(decodeURIComponent(c.req.query("page") || "")) || 1; - const data = await hianime.getGenreAnime(genreName, page); + const data = await cache.getOrSet<HiAnime.ScrapedGenreAnime>( + cacheConfig.key, + async () => hianime.getGenreAnime(genreName, page), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/producer/{name}?page={page} hianimeRouter.get("/producer/:name", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const producerName = decodeURIComponent(c.req.param("name").trim()); const page: number = Number(decodeURIComponent(c.req.query("page") || "")) || 1; - const data = await hianime.getProducerAnimes(producerName, page); + const data = await cache.getOrSet<HiAnime.ScrapedProducerAnime>( + cacheConfig.key, + async () => hianime.getProducerAnimes(producerName, page), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/schedule?date={date} hianimeRouter.get("/schedule", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const date = decodeURIComponent(c.req.query("date") || ""); - const data = await hianime.getEstimatedSchedule(date); + const data = await cache.getOrSet<HiAnime.ScrapedEstimatedSchedule>( + cacheConfig.key, + async () => hianime.getEstimatedSchedule(date), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/search?q={query}&page={page}&filters={...filters} hianimeRouter.get("/search", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); let { q: query, page, ...filters } = c.req.query(); query = decodeURIComponent(query || ""); const pageNo = Number(decodeURIComponent(page || "")) || 1; - const data = await hianime.search(query, pageNo, filters); + const data = await cache.getOrSet<HiAnime.ScrapedAnimeSearchResult>( + cacheConfig.key, + async () => hianime.search(query, pageNo, filters), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/search/suggestion?q={query} hianimeRouter.get("/search/suggestion", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const query = decodeURIComponent(c.req.query("q") || ""); - const data = await hianime.searchSuggestions(query); + const data = await cache.getOrSet<HiAnime.ScrapedAnimeSearchSuggestion>( + cacheConfig.key, + async () => hianime.searchSuggestions(query), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/anime/{animeId} hianimeRouter.get("/anime/:animeId", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const animeId = decodeURIComponent(c.req.param("animeId").trim()); - const data = await hianime.getInfo(animeId); + + const data = await cache.getOrSet<HiAnime.ScrapedAnimeAboutInfo>( + cacheConfig.key, + async () => hianime.getInfo(animeId), + cacheConfig.duration + ); return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/episode/servers?animeEpisodeId={id} hianimeRouter.get("/episode/servers", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const animeEpisodeId = decodeURIComponent( c.req.query("animeEpisodeId") || "" ); - const data = await hianime.getEpisodeServers(animeEpisodeId); + const data = await cache.getOrSet<HiAnime.ScrapedEpisodeServers>( + cacheConfig.key, + async () => hianime.getEpisodeServers(animeEpisodeId), + cacheConfig.duration + ); + return c.json({ success: true, data }, { status: 200 }); }); // episodeId=steinsgate-3?ep=230 // /api/v2/hianime/episode/sources?animeEpisodeId={episodeId}?server={server}&category={category (dub or sub)} hianimeRouter.get("/episode/sources", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const animeEpisodeId = decodeURIComponent( c.req.query("animeEpisodeId") || "" ); const server = decodeURIComponent( c.req.query("server") || HiAnime.Servers.VidStreaming ) as HiAnime.AnimeServers; - const category = decodeURIComponent(c.req.query("category") || "sub") as | "sub" | "dub" | "raw"; - const data = await hianime.getEpisodeSources( - animeEpisodeId, - server, - category + const data = await cache.getOrSet<HiAnime.ScrapedAnimeEpisodesSources>( + cacheConfig.key, + async () => hianime.getEpisodeSources(animeEpisodeId, server, category), + cacheConfig.duration ); + return c.json({ success: true, data }, { status: 200 }); }); // /api/v2/hianime/anime/{anime-id}/episodes hianimeRouter.get("/anime/:animeId/episodes", async (c) => { + const cacheConfig = c.get("CACHE_CONFIG"); const animeId = decodeURIComponent(c.req.param("animeId").trim()); - const data = await hianime.getEpisodes(animeId); + + const data = await cache.getOrSet<HiAnime.ScrapedAnimeEpisodes>( + cacheConfig.key, + async () => hianime.getEpisodes(animeId), + cacheConfig.duration + ); return c.json({ success: true, data }, { status: 200 }); }); diff --git a/src/server.ts b/src/server.ts index 6a9c06e..1b69c39 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,12 +6,14 @@ import { ratelimit } from "./config/ratelimit.js"; import { hianimeRouter } from "./routes/hianime.js"; -import { serveStatic } from "@hono/node-server/serve-static"; -import { serve } from "@hono/node-server"; import { Hono } from "hono"; import { logger } from "hono/logger"; +import { serve } from "@hono/node-server"; +import { serveStatic } from "@hono/node-server/serve-static"; import { HiAnimeError } from "aniwatch"; +import { AniwatchAPICache } from "./config/cache.js"; +import type { AniwatchAPIVariables } from "./config/variables.js"; config(); @@ -19,7 +21,7 @@ const BASE_PATH = "/api/v2" as const; const PORT: number = Number(process.env.ANIWATCH_API_PORT) || 4000; const ANIWATCH_API_HOSTNAME = process.env?.ANIWATCH_API_HOSTNAME; -const app = new Hono(); +const app = new Hono<{ Variables: AniwatchAPIVariables }>(); app.use(logger()); app.use(corsConfig); @@ -35,6 +37,20 @@ if (ISNT_PERSONAL_DEPLOYMENT) { app.use("/", serveStatic({ root: "public" })); app.get("/health", (c) => c.text("OK", { status: 200 })); +app.use(async (c, next) => { + const { pathname, search } = new URL(c.req.url); + + c.set("CACHE_CONFIG", { + key: `${pathname.slice(BASE_PATH.length) + search}`, + duration: Number( + c.req.header(AniwatchAPICache.CACHE_EXPIRY_HEADER_NAME) || + AniwatchAPICache.DEFAULT_CACHE_EXPIRY_SECONDS + ), + }); + + await next(); +}); + app.basePath(BASE_PATH).route("/hianime", hianimeRouter); app .basePath(BASE_PATH) @@ -57,7 +73,7 @@ app.onError((err, c) => { }); // NOTE: this env is "required" for vercel deployments -if (!Boolean(process?.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) { +if (!Boolean(process.env?.ANIWATCH_API_VERCEL_DEPLOYMENT)) { serve({ port: PORT, fetch: app.fetch, |
