mirror of
https://github.com/khairul169/home-lab.git
synced 2025-04-28 08:39:34 +07:00
feat: add api files caching, update audioplayer ui
This commit is contained in:
parent
33c91ac0a7
commit
339969baa8
84
backend/lib/lruCache.ts
Normal file
84
backend/lib/lruCache.ts
Normal 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
backend/middlewares/cache.ts
Normal file
42
backend/middlewares/cache.ts
Normal 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;
|
@ -27,6 +27,7 @@
|
|||||||
"mime": "^4.0.1",
|
"mime": "^4.0.1",
|
||||||
"nanoid": "^5.0.6",
|
"nanoid": "^5.0.6",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
|
"node-id3": "^0.2.6",
|
||||||
"node-pty": "^1.0.0",
|
"node-pty": "^1.0.0",
|
||||||
"systeminformation": "^5.22.2",
|
"systeminformation": "^5.22.2",
|
||||||
"wol": "^1.0.7",
|
"wol": "^1.0.7",
|
||||||
|
64
backend/routes/files/id3Tags.ts
Normal file
64
backend/routes/files/id3Tags.ts
Normal 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!" });
|
||||||
|
}
|
||||||
|
};
|
@ -4,12 +4,18 @@ import { getFilesSchema, ytdlSchema } from "./schema";
|
|||||||
import { getFiles } from "./get";
|
import { getFiles } from "./get";
|
||||||
import { download } from "./download";
|
import { download } from "./download";
|
||||||
import { getYtdl, ytdl } from "./ytdl";
|
import { getYtdl, ytdl } from "./ytdl";
|
||||||
|
import cache from "../../middlewares/cache";
|
||||||
|
import { getId3Tags, getId3Image } from "./id3Tags";
|
||||||
|
|
||||||
|
const cacheFile = cache({ ttl: 86400 });
|
||||||
|
|
||||||
const route = new Hono()
|
const route = new Hono()
|
||||||
.get("/", zValidator("query", getFilesSchema), getFiles)
|
.get("/", zValidator("query", getFilesSchema), getFiles)
|
||||||
.post("/upload")
|
.post("/upload")
|
||||||
.post("/ytdl", zValidator("json", ytdlSchema), ytdl)
|
.post("/ytdl", zValidator("json", ytdlSchema), ytdl)
|
||||||
.get("/ytdl/:id", getYtdl)
|
.get("/ytdl/:id", getYtdl)
|
||||||
.get("/download/*", download);
|
.get("/download/*", cacheFile, download)
|
||||||
|
.get("/id3-tags/*", cacheFile, getId3Tags)
|
||||||
|
.get("/id3-img/*", cacheFile, getId3Image);
|
||||||
|
|
||||||
export default route;
|
export default route;
|
||||||
|
@ -260,6 +260,13 @@ hono@^4.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/hono/-/hono-4.1.0.tgz#62cef81df0dbf731643155e1e5c1b9dffb230dc4"
|
resolved "https://registry.yarnpkg.com/hono/-/hono-4.1.0.tgz#62cef81df0dbf731643155e1e5c1b9dffb230dc4"
|
||||||
integrity sha512-9no6DCHb4ijB1tWdFXU6JnrnFgzwVZ1cnIcS1BjAFnMcjbtBTOMsQrDrPH3GXbkNEEEkj8kWqcYBy8Qc0bBkJQ==
|
integrity sha512-9no6DCHb4ijB1tWdFXU6JnrnFgzwVZ1cnIcS1BjAFnMcjbtBTOMsQrDrPH3GXbkNEEEkj8kWqcYBy8Qc0bBkJQ==
|
||||||
|
|
||||||
|
iconv-lite@0.6.2:
|
||||||
|
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:
|
isexe@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
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"
|
fetch-blob "^3.1.4"
|
||||||
formdata-polyfill "^4.0.10"
|
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:
|
node-pty@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.0.0.tgz#7daafc0aca1c4ca3de15c61330373af4af5861fd"
|
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"
|
resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f"
|
||||||
integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==
|
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:
|
sax@^1.1.3, sax@^1.2.4:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0"
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
"expo-router": "~3.4.8",
|
"expo-router": "~3.4.8",
|
||||||
"expo-status-bar": "~1.11.1",
|
"expo-status-bar": "~1.11.1",
|
||||||
"hono": "^4.1.0",
|
"hono": "^4.1.0",
|
||||||
"jsmediatags": "^3.9.7",
|
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import "./styles.css";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Stack, router, usePathname } from "expo-router";
|
import { Stack, router, usePathname } from "expo-router";
|
||||||
import { QueryClientProvider } from "react-query";
|
import { QueryClientProvider } from "react-query";
|
||||||
|
@ -114,6 +114,7 @@ const FilesPage = () => {
|
|||||||
|
|
||||||
const fileType = getFileType(file.path);
|
const fileType = getFileType(file.path);
|
||||||
if (fileType === "audio") {
|
if (fileType === "audio") {
|
||||||
|
audioPlayer.expand();
|
||||||
return audioPlayer.play(data, idx);
|
return audioPlayer.play(data, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
src/app/styles.css
Normal file
20
src/app/styles.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #6366f1;
|
||||||
|
border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports not selector(::-webkit-scrollbar) {
|
||||||
|
* {
|
||||||
|
scrollbar-color: #6366f1 #232e33;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import { getFileUrl } from "@/app/apps/lib";
|
import { getFileUrl } from "@/app/apps/lib";
|
||||||
|
import { fetchAPI } from "@/lib/api";
|
||||||
|
import { API_BASEURL } from "@/lib/constants";
|
||||||
import { base64encode } from "@/lib/utils";
|
import { base64encode } from "@/lib/utils";
|
||||||
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
|
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
|
||||||
|
import authStore from "@/stores/authStore";
|
||||||
import { AVPlaybackStatusSuccess, Audio } from "expo-av";
|
import { AVPlaybackStatusSuccess, Audio } from "expo-av";
|
||||||
import jsmediatags from "jsmediatags/build2/jsmediatags";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useStore } from "zustand";
|
import { useStore } from "zustand";
|
||||||
|
|
||||||
@ -48,29 +50,18 @@ const AudioPlayerProvider = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadMediaTags() {
|
async function loadMediaTags() {
|
||||||
const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
|
|
||||||
audioPlayerStore.setState({ mediaTags: null });
|
audioPlayerStore.setState({ mediaTags: null });
|
||||||
|
|
||||||
tagsReader.read({
|
try {
|
||||||
onSuccess: (result: any) => {
|
const url =
|
||||||
const mediaTagsResult = { ...result };
|
`${API_BASEURL}/files/id3-tags${fileData.path}?token=` +
|
||||||
|
authStore.getState().token;
|
||||||
|
|
||||||
if (result?.tags?.picture) {
|
const res = await fetchAPI(url);
|
||||||
const { data, format } = result.tags.picture;
|
const data = await res.json();
|
||||||
let base64String = "";
|
audioPlayerStore.setState({ mediaTags: data });
|
||||||
for (let i = 0; i < data.length; i++) {
|
} catch (err) {}
|
||||||
base64String += String.fromCharCode(data[i]);
|
|
||||||
}
|
|
||||||
mediaTagsResult.picture = `data:${format};base64,${base64encode(
|
|
||||||
base64String
|
|
||||||
)}`;
|
|
||||||
delete data?.tags?.picture;
|
|
||||||
}
|
|
||||||
|
|
||||||
audioPlayerStore.setState({ mediaTags: mediaTagsResult });
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMediaTags();
|
loadMediaTags();
|
||||||
|
@ -13,6 +13,12 @@ import Input from "@ui/Input";
|
|||||||
import { useStore } from "zustand";
|
import { useStore } from "zustand";
|
||||||
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
|
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
|
||||||
import Modal from "react-native-modal";
|
import Modal from "react-native-modal";
|
||||||
|
import { getFileUrl } from "@/app/apps/lib";
|
||||||
|
import { useQuery } from "react-query";
|
||||||
|
import { API_BASEURL } from "@/lib/constants";
|
||||||
|
import authStore from "@/stores/authStore";
|
||||||
|
import { fetchAPI } from "@/lib/api";
|
||||||
|
import { MediaTags } from "@/types/mediaTags";
|
||||||
|
|
||||||
const AudioPlayer = () => {
|
const AudioPlayer = () => {
|
||||||
const expanded = useStore(audioPlayerStore, (i) => i.expanded);
|
const expanded = useStore(audioPlayerStore, (i) => i.expanded);
|
||||||
@ -41,7 +47,7 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
|||||||
<Box className="flex-1 items-stretch relative">
|
<Box className="flex-1 items-stretch relative">
|
||||||
<Box className="absolute -inset-10 -z-[1]">
|
<Box className="absolute -inset-10 -z-[1]">
|
||||||
<Image
|
<Image
|
||||||
source={mediaTags?.picture ? { uri: mediaTags?.picture } : bgImage}
|
source={mediaTags?.image ? { uri: mediaTags.image } : bgImage}
|
||||||
style={cn("absolute -inset-5 w-full h-full")}
|
style={cn("absolute -inset-5 w-full h-full")}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
blurRadius={10}
|
blurRadius={10}
|
||||||
@ -58,11 +64,11 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
|||||||
onPress={onClose}
|
onPress={onClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box className="flex flex-col items-center justify-center p-8 flex-1 overflow-hidden">
|
<Box className="flex flex-col items-center justify-center p-8 pt-20 flex-1 overflow-hidden">
|
||||||
{mediaTags?.picture ? (
|
{mediaTags?.image ? (
|
||||||
<Box className="aspect-square flex-1 max-h-[400px] mb-8">
|
<Box className="aspect-square flex-1 max-h-[400px] mb-8">
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: mediaTags.picture }}
|
source={{ uri: mediaTags.image }}
|
||||||
style={cn("w-full h-full")}
|
style={cn("w-full h-full")}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
/>
|
/>
|
||||||
@ -74,11 +80,11 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
|||||||
className="text-white text-xl md:text-3xl mt-4"
|
className="text-white text-xl md:text-3xl mt-4"
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{mediaTags?.tags?.title || filename}
|
{mediaTags?.title || filename}
|
||||||
</Text>
|
</Text>
|
||||||
{mediaTags?.tags?.artist ? (
|
{mediaTags?.artist ? (
|
||||||
<Text className="text-white mt-2" numberOfLines={1}>
|
<Text className="text-white mt-2" numberOfLines={1}>
|
||||||
{mediaTags.tags.artist}
|
{mediaTags.artist}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@ -157,9 +163,9 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const PLAYLIST_ITEM_HEIGHT = 49;
|
const PLAYLIST_ITEM_HEIGHT = 82;
|
||||||
|
|
||||||
const Playlist = () => {
|
const Playlist = React.memo(() => {
|
||||||
const playlist = useStore(audioPlayerStore, (i) => i.playlist);
|
const playlist = useStore(audioPlayerStore, (i) => i.playlist);
|
||||||
const currentIdx = useStore(audioPlayerStore, (i) => i.currentIdx);
|
const currentIdx = useStore(audioPlayerStore, (i) => i.currentIdx);
|
||||||
const containerRef = useRef<any>();
|
const containerRef = useRef<any>();
|
||||||
@ -204,9 +210,8 @@ const Playlist = () => {
|
|||||||
|
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
|
contentContainerStyle={cn("px-4")}
|
||||||
data={list}
|
data={list}
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
keyExtractor={(i) => i.path}
|
keyExtractor={(i) => i.path}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<PlaylistItem
|
<PlaylistItem
|
||||||
@ -221,7 +226,7 @@ const Playlist = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
type PlaylistItemProps = {
|
type PlaylistItemProps = {
|
||||||
file: FileItem;
|
file: FileItem;
|
||||||
@ -230,17 +235,43 @@ type PlaylistItemProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
|
const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
|
||||||
|
const url =
|
||||||
|
`${API_BASEURL}/files/id3-tags${file.path}?token=` +
|
||||||
|
authStore.getState().token;
|
||||||
|
const { data: id3Tags } = useQuery<MediaTags>({
|
||||||
|
queryKey: ["id3-tags", file.path],
|
||||||
|
queryFn: () => fetchAPI(url).then((i) => i.json()),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
style={cn(
|
style={cn(
|
||||||
"py-4 px-5 border-b border-white/10",
|
"mb-4 flex flex-row items-center gap-4 border border-transparent",
|
||||||
isCurrent && "bg-[#323232]"
|
isCurrent && "bg-white/10 border-white/20 rounded-lg overflow-hidden"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Text className="text-white" numberOfLines={1}>
|
<Box className="bg-gray-800 w-16 h-16 flex items-center justify-center">
|
||||||
{file.name}
|
{id3Tags?.image ? (
|
||||||
</Text>
|
<Image
|
||||||
|
source={{ uri: id3Tags.image }}
|
||||||
|
style={cn("w-full h-full")}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Ionicons name="musical-notes" style={cn("text-white text-2xl")} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box className="py-2 flex-1">
|
||||||
|
<Text className="text-white font-bold" numberOfLines={1}>
|
||||||
|
{id3Tags?.title || file.name}
|
||||||
|
</Text>
|
||||||
|
{id3Tags?.artist ? (
|
||||||
|
<Text className="text-white mt-2" numberOfLines={1}>
|
||||||
|
{id3Tags?.artist}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -33,22 +33,14 @@ export class ApiError extends Error {
|
|||||||
|
|
||||||
async function fetchHandler(input: any, init?: RequestInit) {
|
async function fetchHandler(input: any, init?: RequestInit) {
|
||||||
const token = authStore.getState().token;
|
const token = authStore.getState().token;
|
||||||
|
const config = typeof input === "object" ? input : init || {};
|
||||||
|
|
||||||
if (init) {
|
config.headers = new Headers(config.headers);
|
||||||
init.headers = new Headers(init.headers);
|
if (token) {
|
||||||
if (token) {
|
config.headers.set("Authorization", `Bearer ${token}`);
|
||||||
init.headers.set("Authorization", `Bearer ${token}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof input === "object") {
|
const res = await fetch(input, config);
|
||||||
input.headers = new Headers(init.headers);
|
|
||||||
if (token) {
|
|
||||||
input.headers.set("Authorization", `Bearer ${token}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(input, init);
|
|
||||||
await checkResponse(res);
|
await checkResponse(res);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
@ -84,5 +76,6 @@ export {
|
|||||||
InferResponseType,
|
InferResponseType,
|
||||||
ClientRequestOptions,
|
ClientRequestOptions,
|
||||||
ClientResponse,
|
ClientResponse,
|
||||||
|
fetchHandler as fetchAPI,
|
||||||
};
|
};
|
||||||
export default api;
|
export default api;
|
||||||
|
@ -101,6 +101,10 @@ const seek = async (progress: number) => {
|
|||||||
sound.setPositionAsync(pos);
|
sound.setPositionAsync(pos);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const expand = () => {
|
||||||
|
audioPlayerStore.setState({ expanded: true });
|
||||||
|
};
|
||||||
|
|
||||||
export const audioPlayer = {
|
export const audioPlayer = {
|
||||||
store: audioPlayerStore,
|
store: audioPlayerStore,
|
||||||
play,
|
play,
|
||||||
@ -108,4 +112,5 @@ export const audioPlayer = {
|
|||||||
prev,
|
prev,
|
||||||
next,
|
next,
|
||||||
seek,
|
seek,
|
||||||
|
expand,
|
||||||
};
|
};
|
||||||
|
@ -1,83 +1,18 @@
|
|||||||
export type MediaTags = {
|
export type MediaTags = {
|
||||||
type: string;
|
|
||||||
version: string;
|
|
||||||
major: number;
|
|
||||||
revision: number;
|
|
||||||
flags: Flags;
|
|
||||||
size: number;
|
|
||||||
tags: Tags;
|
|
||||||
picture?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Flags = {
|
|
||||||
unsynchronisation: boolean;
|
|
||||||
extended_header: boolean;
|
|
||||||
experimental_indicator: boolean;
|
|
||||||
footer_present: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Tags = {
|
|
||||||
title: string;
|
title: string;
|
||||||
artist: string;
|
|
||||||
album: string;
|
album: string;
|
||||||
year: string;
|
|
||||||
comment: Comment;
|
|
||||||
track: string;
|
|
||||||
genre: string;
|
genre: string;
|
||||||
// picture: Picture;
|
year: string;
|
||||||
TALB: Talb;
|
trackNumber: string;
|
||||||
TPE1: Talb;
|
partOfSet: string;
|
||||||
COMM: Comm;
|
artist: string;
|
||||||
TCON: Talb;
|
performerInfo: string;
|
||||||
TIT2: Talb;
|
composer: string;
|
||||||
TRCK: Talb;
|
userDefinedText: UserDefinedText[];
|
||||||
TYER: Talb;
|
image: string;
|
||||||
TXXX: Txxx;
|
|
||||||
APIC: APIC;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type APIC = {
|
export type UserDefinedText = {
|
||||||
id: string;
|
|
||||||
size: number;
|
|
||||||
description: string;
|
description: string;
|
||||||
data: Picture;
|
value: string;
|
||||||
};
|
|
||||||
|
|
||||||
export type Picture = {
|
|
||||||
format: string;
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
data: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Comm = {
|
|
||||||
id: string;
|
|
||||||
size: number;
|
|
||||||
description: string;
|
|
||||||
data: Comment;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Comment = {
|
|
||||||
language: string;
|
|
||||||
short_description: string;
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Talb = {
|
|
||||||
id: string;
|
|
||||||
size: number;
|
|
||||||
description: string;
|
|
||||||
data: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Txxx = {
|
|
||||||
id: string;
|
|
||||||
size: number;
|
|
||||||
description: string;
|
|
||||||
data: Data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Data = {
|
|
||||||
user_description: string;
|
|
||||||
data: string;
|
|
||||||
};
|
};
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -6320,13 +6320,6 @@ jsesc@~0.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||||
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
|
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
|
||||||
|
|
||||||
jsmediatags@^3.9.7:
|
|
||||||
version "3.9.7"
|
|
||||||
resolved "https://registry.yarnpkg.com/jsmediatags/-/jsmediatags-3.9.7.tgz#808c6713b5ccb9712a4dc4b2149a0cb253c641a2"
|
|
||||||
integrity sha512-xCAO8C3li3t5hYkXqn8iv8zQQUB4T1QqRN2aSONHMls21ICdEvXi4xtb6W70/fAFYSDwMHd32hIqvo4YuXoNcQ==
|
|
||||||
dependencies:
|
|
||||||
xhr2 "^0.1.4"
|
|
||||||
|
|
||||||
json-buffer@3.0.0:
|
json-buffer@3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||||
@ -10084,11 +10077,6 @@ xdg-basedir@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
|
||||||
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
|
||||||
|
|
||||||
xhr2@^0.1.4:
|
|
||||||
version "0.1.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
|
|
||||||
integrity sha512-3QGhDryRzTbIDj+waTRvMBe8SyPhW79kz3YnNb+HQt/6LPYQT3zT3Jt0Y8pBofZqQX26x8Ecfv0FXR72uH5VpA==
|
|
||||||
|
|
||||||
xml2js@0.6.0:
|
xml2js@0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user