feat: add api files caching, update audioplayer ui

This commit is contained in:
2024-03-25 06:59:40 +07:00
parent 33c91ac0a7
commit 339969baa8
16 changed files with 320 additions and 140 deletions
+84
View File
@@ -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;
+42
View File
@@ -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;
+1
View File
@@ -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",
+64
View File
@@ -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!" });
}
};
+7 -1
View File
@@ -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;
+19
View File
@@ -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"