mirror of
https://github.com/khairul169/home-lab.git
synced 2026-10-02 17:13:57 +07:00
feat: add api files caching, update audioplayer ui
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
// Adapted from https://levelup.gitconnected.com/implementing-lru-cache-with-node-js-and-typescript-a7c8d3f6a63
|
||||
|
||||
// Define an interface for the cache items with key-value pairs and expiry time
|
||||
interface CacheItem<T> {
|
||||
key: string;
|
||||
value: T;
|
||||
expiryTime: number;
|
||||
}
|
||||
|
||||
// Create a generic LRU cache class with ttl support
|
||||
class LRU<T> {
|
||||
// Define the maximum cache size and the cache data structure
|
||||
private readonly maxSize: number;
|
||||
private cache: Map<string, CacheItem<T>>;
|
||||
private checkIntervalHandler: NodeJS.Timeout | null = null;
|
||||
|
||||
// Initialize the LRU cache with a specified maximum size and ttl
|
||||
constructor(maxSize: number, checkInterval: number = 3600) {
|
||||
this.maxSize = maxSize;
|
||||
this.cache = new Map<string, CacheItem<T>>();
|
||||
this.checkIntervalHandler = setInterval(
|
||||
this.check.bind(this),
|
||||
checkInterval * 1000
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.cache = new Map<string, CacheItem<T>>();
|
||||
if (this.checkIntervalHandler) {
|
||||
clearInterval(this.checkIntervalHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Add an item to the cache, evicting the least recently used item if the cache is full
|
||||
set(key: string, value: T, ttl: number = 3600): void {
|
||||
const expiryTime = Date.now() + ttl * 1000;
|
||||
|
||||
// find the least recently used item and remove it from the cache
|
||||
// get the list of keys in the cache and get the first one
|
||||
if (this.cache.size >= this.maxSize) {
|
||||
const lruKey = this.cache.keys().next().value;
|
||||
// remove the least recently used item from the cache
|
||||
this.cache.delete(lruKey);
|
||||
}
|
||||
|
||||
this.cache.set(key, { key, value, expiryTime });
|
||||
}
|
||||
|
||||
// Retrieve an item from the cache, and update its position as the most recently used item
|
||||
get(key: string): T | undefined {
|
||||
const item = this.cache.get(key);
|
||||
|
||||
if (item && item.expiryTime > Date.now()) {
|
||||
this.cache.delete(key);
|
||||
this.cache.set(key, item);
|
||||
return item.value;
|
||||
} else {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for expired caches and delete it
|
||||
check() {
|
||||
const now = Date.now();
|
||||
for (const [key, item] of this.cache) {
|
||||
if (item.expiryTime < now) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an item from the cache by its key
|
||||
delete(key: string): void {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
|
||||
// Clear the cache
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export default LRU;
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Context, Next } from "hono";
|
||||
import LRU from "../lib/lruCache";
|
||||
|
||||
type CacheOptions = {
|
||||
ttl?: number;
|
||||
noCache?: boolean;
|
||||
public?: boolean;
|
||||
store?: boolean;
|
||||
};
|
||||
|
||||
const caches = new LRU<string>(50);
|
||||
|
||||
const cache = (options: CacheOptions = {}) => {
|
||||
return (c: Context, next: Next) => {
|
||||
const ttl = options.ttl || 3600;
|
||||
|
||||
const cacheControl = !options.noCache
|
||||
? `public, max-age=${ttl}`
|
||||
: "no-cache";
|
||||
c.header("Cache-Control", cacheControl);
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
cache.key = (...keys: string[]) => {
|
||||
const key = keys.join(".");
|
||||
const data = cache.get(key);
|
||||
const set = (value: any, ttl: number = 3600) => cache.set(key, value, ttl);
|
||||
return { key, data, set };
|
||||
};
|
||||
|
||||
cache.set = (key: string, value: any, ttl: number = 3600) => {
|
||||
caches.set(key, JSON.stringify(value), ttl);
|
||||
};
|
||||
|
||||
cache.get = (key: string) => {
|
||||
const data = caches.get(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
};
|
||||
|
||||
export default cache;
|
||||
@@ -27,6 +27,7 @@
|
||||
"mime": "^4.0.1",
|
||||
"nanoid": "^5.0.6",
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-id3": "^0.2.6",
|
||||
"node-pty": "^1.0.0",
|
||||
"systeminformation": "^5.22.2",
|
||||
"wol": "^1.0.7",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { Context } from "hono";
|
||||
import { getFilePath } from "./utils";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import ID3 from "node-id3";
|
||||
import cache from "../../middlewares/cache";
|
||||
|
||||
export const getId3Tags = async (c: Context) => {
|
||||
const url = new URL(c.req.url);
|
||||
const pathname = decodeURI(url.pathname).split("/");
|
||||
const uri = pathname.slice(pathname.indexOf("id3-tags") + 1).join("/");
|
||||
const { path } = getFilePath("/" + uri);
|
||||
|
||||
const cached = cache.key("id3-tags", path);
|
||||
if (cached.data != null) {
|
||||
return c.json(cached.data || {});
|
||||
}
|
||||
|
||||
let tags: any = false;
|
||||
|
||||
try {
|
||||
const id3Tags = await ID3.Promise.read(path, { noRaw: true });
|
||||
const data: any = { ...id3Tags };
|
||||
|
||||
if (data.image) {
|
||||
const imgUrl = new URL(url);
|
||||
imgUrl.pathname = imgUrl.pathname.replace("/id3-tags", "/id3-img");
|
||||
data.image = imgUrl.toString();
|
||||
}
|
||||
|
||||
tags = id3Tags;
|
||||
} catch (err) {}
|
||||
|
||||
cached.set(tags);
|
||||
|
||||
return c.json(tags || {});
|
||||
};
|
||||
|
||||
export const getId3Image = async (c: Context) => {
|
||||
const url = new URL(c.req.url);
|
||||
const pathname = decodeURI(url.pathname).split("/");
|
||||
const uri = pathname.slice(pathname.indexOf("id3-img") + 1).join("/");
|
||||
const { path } = getFilePath("/" + uri);
|
||||
|
||||
const cached = cache.key("id3-img", path);
|
||||
if (cached.data) {
|
||||
c.header("Content-Type", cached.data.mime);
|
||||
const buffer = Buffer.from(cached.data.imageBuffer);
|
||||
return c.body(buffer);
|
||||
}
|
||||
|
||||
try {
|
||||
const tags = await ID3.Promise.read(path, { noRaw: true });
|
||||
const image = tags.image;
|
||||
if (!image || typeof image === "string") {
|
||||
throw new Error("No image found!");
|
||||
}
|
||||
cached.set(image);
|
||||
|
||||
c.header("Content-Type", image.mime);
|
||||
return c.body(image.imageBuffer);
|
||||
} catch (err) {
|
||||
throw new HTTPException(400, { message: "cannot get tags!" });
|
||||
}
|
||||
};
|
||||
@@ -4,12 +4,18 @@ import { getFilesSchema, ytdlSchema } from "./schema";
|
||||
import { getFiles } from "./get";
|
||||
import { download } from "./download";
|
||||
import { getYtdl, ytdl } from "./ytdl";
|
||||
import cache from "../../middlewares/cache";
|
||||
import { getId3Tags, getId3Image } from "./id3Tags";
|
||||
|
||||
const cacheFile = cache({ ttl: 86400 });
|
||||
|
||||
const route = new Hono()
|
||||
.get("/", zValidator("query", getFilesSchema), getFiles)
|
||||
.post("/upload")
|
||||
.post("/ytdl", zValidator("json", ytdlSchema), ytdl)
|
||||
.get("/ytdl/:id", getYtdl)
|
||||
.get("/download/*", download);
|
||||
.get("/download/*", cacheFile, download)
|
||||
.get("/id3-tags/*", cacheFile, getId3Tags)
|
||||
.get("/id3-img/*", cacheFile, getId3Image);
|
||||
|
||||
export default route;
|
||||
|
||||
@@ -260,6 +260,13 @@ hono@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/hono/-/hono-4.1.0.tgz#62cef81df0dbf731643155e1e5c1b9dffb230dc4"
|
||||
integrity sha512-9no6DCHb4ijB1tWdFXU6JnrnFgzwVZ1cnIcS1BjAFnMcjbtBTOMsQrDrPH3GXbkNEEEkj8kWqcYBy8Qc0bBkJQ==
|
||||
|
||||
[email protected]:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
|
||||
integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
@@ -312,6 +319,13 @@ node-fetch@^3.3.2:
|
||||
fetch-blob "^3.1.4"
|
||||
formdata-polyfill "^4.0.10"
|
||||
|
||||
node-id3@^0.2.6:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/node-id3/-/node-id3-0.2.6.tgz#d149517bc40c7974845d31bee15177762b416ebc"
|
||||
integrity sha512-w8GuKXLlPpDjTxLowCt/uYMhRQzED3cg2GdSG1i6RSGKeDzPvxlXeLQuQInKljahPZ0aDnmyX7FX8BbJOM7REg==
|
||||
dependencies:
|
||||
iconv-lite "0.6.2"
|
||||
|
||||
node-pty@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.0.0.tgz#7daafc0aca1c4ca3de15c61330373af4af5861fd"
|
||||
@@ -324,6 +338,11 @@ resolve-pkg-maps@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
|
||||
integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3.0.0":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
sax@^1.1.3, sax@^1.2.4:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
|
||||
|
||||
Reference in New Issue
Block a user