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",
|
||||
"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
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 { 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==
|
||||
|
||||
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:
|
||||
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"
|
||||
|
@ -26,7 +26,6 @@
|
||||
"expo-router": "~3.4.8",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"hono": "^4.1.0",
|
||||
"jsmediatags": "^3.9.7",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-helmet": "^6.1.0",
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "./styles.css";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Stack, router, usePathname } from "expo-router";
|
||||
import { QueryClientProvider } from "react-query";
|
||||
|
@ -114,6 +114,7 @@ const FilesPage = () => {
|
||||
|
||||
const fileType = getFileType(file.path);
|
||||
if (fileType === "audio") {
|
||||
audioPlayer.expand();
|
||||
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 { fetchAPI } from "@/lib/api";
|
||||
import { API_BASEURL } from "@/lib/constants";
|
||||
import { base64encode } from "@/lib/utils";
|
||||
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
|
||||
import authStore from "@/stores/authStore";
|
||||
import { AVPlaybackStatusSuccess, Audio } from "expo-av";
|
||||
import jsmediatags from "jsmediatags/build2/jsmediatags";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useStore } from "zustand";
|
||||
|
||||
@ -48,29 +50,18 @@ const AudioPlayerProvider = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function loadMediaTags() {
|
||||
const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
|
||||
async function loadMediaTags() {
|
||||
audioPlayerStore.setState({ mediaTags: null });
|
||||
|
||||
tagsReader.read({
|
||||
onSuccess: (result: any) => {
|
||||
const mediaTagsResult = { ...result };
|
||||
try {
|
||||
const url =
|
||||
`${API_BASEURL}/files/id3-tags${fileData.path}?token=` +
|
||||
authStore.getState().token;
|
||||
|
||||
if (result?.tags?.picture) {
|
||||
const { data, format } = result.tags.picture;
|
||||
let base64String = "";
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
base64String += String.fromCharCode(data[i]);
|
||||
}
|
||||
mediaTagsResult.picture = `data:${format};base64,${base64encode(
|
||||
base64String
|
||||
)}`;
|
||||
delete data?.tags?.picture;
|
||||
}
|
||||
|
||||
audioPlayerStore.setState({ mediaTags: mediaTagsResult });
|
||||
},
|
||||
});
|
||||
const res = await fetchAPI(url);
|
||||
const data = await res.json();
|
||||
audioPlayerStore.setState({ mediaTags: data });
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
loadMediaTags();
|
||||
|
@ -13,6 +13,12 @@ import Input from "@ui/Input";
|
||||
import { useStore } from "zustand";
|
||||
import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
|
||||
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 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="absolute -inset-10 -z-[1]">
|
||||
<Image
|
||||
source={mediaTags?.picture ? { uri: mediaTags?.picture } : bgImage}
|
||||
source={mediaTags?.image ? { uri: mediaTags.image } : bgImage}
|
||||
style={cn("absolute -inset-5 w-full h-full")}
|
||||
resizeMode="cover"
|
||||
blurRadius={10}
|
||||
@ -58,11 +64,11 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
||||
onPress={onClose}
|
||||
/>
|
||||
|
||||
<Box className="flex flex-col items-center justify-center p-8 flex-1 overflow-hidden">
|
||||
{mediaTags?.picture ? (
|
||||
<Box className="flex flex-col items-center justify-center p-8 pt-20 flex-1 overflow-hidden">
|
||||
{mediaTags?.image ? (
|
||||
<Box className="aspect-square flex-1 max-h-[400px] mb-8">
|
||||
<Image
|
||||
source={{ uri: mediaTags.picture }}
|
||||
source={{ uri: mediaTags.image }}
|
||||
style={cn("w-full h-full")}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
@ -74,11 +80,11 @@ const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
|
||||
className="text-white text-xl md:text-3xl mt-4"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{mediaTags?.tags?.title || filename}
|
||||
{mediaTags?.title || filename}
|
||||
</Text>
|
||||
{mediaTags?.tags?.artist ? (
|
||||
{mediaTags?.artist ? (
|
||||
<Text className="text-white mt-2" numberOfLines={1}>
|
||||
{mediaTags.tags.artist}
|
||||
{mediaTags.artist}
|
||||
</Text>
|
||||
) : 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 currentIdx = useStore(audioPlayerStore, (i) => i.currentIdx);
|
||||
const containerRef = useRef<any>();
|
||||
@ -204,9 +210,8 @@ const Playlist = () => {
|
||||
|
||||
<FlatList
|
||||
ref={containerRef}
|
||||
contentContainerStyle={cn("px-4")}
|
||||
data={list}
|
||||
showsVerticalScrollIndicator={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
keyExtractor={(i) => i.path}
|
||||
renderItem={({ item }) => (
|
||||
<PlaylistItem
|
||||
@ -221,7 +226,7 @@ const Playlist = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
type PlaylistItemProps = {
|
||||
file: FileItem;
|
||||
@ -230,17 +235,43 @@ type 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 (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={cn(
|
||||
"py-4 px-5 border-b border-white/10",
|
||||
isCurrent && "bg-[#323232]"
|
||||
"mb-4 flex flex-row items-center gap-4 border border-transparent",
|
||||
isCurrent && "bg-white/10 border-white/20 rounded-lg overflow-hidden"
|
||||
)}
|
||||
>
|
||||
<Text className="text-white" numberOfLines={1}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Box className="bg-gray-800 w-16 h-16 flex items-center justify-center">
|
||||
{id3Tags?.image ? (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@ -33,22 +33,14 @@ export class ApiError extends Error {
|
||||
|
||||
async function fetchHandler(input: any, init?: RequestInit) {
|
||||
const token = authStore.getState().token;
|
||||
const config = typeof input === "object" ? input : init || {};
|
||||
|
||||
if (init) {
|
||||
init.headers = new Headers(init.headers);
|
||||
if (token) {
|
||||
init.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
config.headers = new Headers(config.headers);
|
||||
if (token) {
|
||||
config.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (typeof input === "object") {
|
||||
input.headers = new Headers(init.headers);
|
||||
if (token) {
|
||||
input.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(input, init);
|
||||
const res = await fetch(input, config);
|
||||
await checkResponse(res);
|
||||
|
||||
return res;
|
||||
@ -84,5 +76,6 @@ export {
|
||||
InferResponseType,
|
||||
ClientRequestOptions,
|
||||
ClientResponse,
|
||||
fetchHandler as fetchAPI,
|
||||
};
|
||||
export default api;
|
||||
|
@ -101,6 +101,10 @@ const seek = async (progress: number) => {
|
||||
sound.setPositionAsync(pos);
|
||||
};
|
||||
|
||||
const expand = () => {
|
||||
audioPlayerStore.setState({ expanded: true });
|
||||
};
|
||||
|
||||
export const audioPlayer = {
|
||||
store: audioPlayerStore,
|
||||
play,
|
||||
@ -108,4 +112,5 @@ export const audioPlayer = {
|
||||
prev,
|
||||
next,
|
||||
seek,
|
||||
expand,
|
||||
};
|
||||
|
@ -1,83 +1,18 @@
|
||||
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;
|
||||
artist: string;
|
||||
album: string;
|
||||
year: string;
|
||||
comment: Comment;
|
||||
track: string;
|
||||
genre: string;
|
||||
// picture: Picture;
|
||||
TALB: Talb;
|
||||
TPE1: Talb;
|
||||
COMM: Comm;
|
||||
TCON: Talb;
|
||||
TIT2: Talb;
|
||||
TRCK: Talb;
|
||||
TYER: Talb;
|
||||
TXXX: Txxx;
|
||||
APIC: APIC;
|
||||
year: string;
|
||||
trackNumber: string;
|
||||
partOfSet: string;
|
||||
artist: string;
|
||||
performerInfo: string;
|
||||
composer: string;
|
||||
userDefinedText: UserDefinedText[];
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type APIC = {
|
||||
id: string;
|
||||
size: number;
|
||||
export type UserDefinedText = {
|
||||
description: string;
|
||||
data: Picture;
|
||||
};
|
||||
|
||||
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;
|
||||
value: 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"
|
||||
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:
|
||||
version "3.0.0"
|
||||
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"
|
||||
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:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"
|
||||
|
Loading…
x
Reference in New Issue
Block a user