feat(frontend,backend): storage panel with weight rows and trash, heavy media streams by url in the viewer
This commit is contained in:
1 parent
f2c245707a
commit
55634984fd
19 files changed
+1822
-24
No files matched your search
@@ -95,6 +95,19 @@ export async function request<T>(
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
function streamUrl(path: string): string {
|
||||
const query = auth.token ? `?token=${encodeURIComponent(auth.token)}` : "";
|
||||
return `${BASE}${path}${query}`;
|
||||
}
|
||||
|
||||
export function mediaUrl(mediaId: number): string {
|
||||
return streamUrl(`/media/${mediaId}`);
|
||||
}
|
||||
|
||||
export function trashedUrl(storageKey: string): string {
|
||||
return streamUrl(`/storage/trash/${storageKey}/file`);
|
||||
}
|
||||
|
||||
export type MediaResult =
|
||||
| { state: "ready"; url: string; mime: string | null }
|
||||
| { state: "not-downloaded" }
|
||||
|
||||
@@ -31,7 +31,12 @@ import type {
|
||||
ReactionView,
|
||||
ResponseStats,
|
||||
SearchHit,
|
||||
StorageFile,
|
||||
StorageGroupName,
|
||||
StorageSummary,
|
||||
StoryView,
|
||||
TrashItem,
|
||||
TrashOutcome,
|
||||
VolumeBucket,
|
||||
Watch,
|
||||
WatchedBot,
|
||||
@@ -581,3 +586,42 @@ export function addWatchedBot(token: string): Promise<WatchedBot> {
|
||||
export function removeWatchedBot(botId: number): Promise<void> {
|
||||
return request<void>(`/bots/${botId}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export function getStorageSummary(): Promise<StorageSummary> {
|
||||
return request<StorageSummary>("/storage/summary", { account: true });
|
||||
}
|
||||
|
||||
export function getStorageFiles(
|
||||
group: StorageGroupName | null,
|
||||
page: Page = {}
|
||||
): Promise<StorageFile[]> {
|
||||
return request<StorageFile[]>("/storage/files", {
|
||||
account: true,
|
||||
query: { group, ...page },
|
||||
});
|
||||
}
|
||||
|
||||
export function getTrash(): Promise<TrashItem[]> {
|
||||
return request<TrashItem[]>("/storage/trash", { account: true });
|
||||
}
|
||||
|
||||
export function moveToTrash(mediaIds: number[]): Promise<TrashOutcome> {
|
||||
return request<TrashOutcome>("/storage/trash", {
|
||||
body: { account_id: accounts.selectedId, media_ids: mediaIds },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreFromTrash(keys: string[]): Promise<TrashOutcome> {
|
||||
return request<TrashOutcome>("/storage/trash/restore", {
|
||||
body: { account_id: accounts.selectedId, keys },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
export function purgeTrash(keys: string[] | null): Promise<TrashOutcome> {
|
||||
return request<TrashOutcome>("/storage/trash/purge", {
|
||||
body: { account_id: accounts.selectedId, keys },
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export interface ViewerItem {
|
||||
mediaId: number | null;
|
||||
messageId: number;
|
||||
mime: string | null;
|
||||
src?: string;
|
||||
}
|
||||
|
||||
export function viewerItemsFrom(
|
||||
@@ -102,6 +103,16 @@ export function visualKind(kind: string): VisualKind {
|
||||
return "other";
|
||||
}
|
||||
|
||||
const STREAM_KINDS = new Set(["video", "video_note", "voice", "audio"]);
|
||||
const STREAM_MIME_PREFIXES = ["video/", "audio/"];
|
||||
|
||||
export function isStreamable(kind: string, mime: string | null): boolean {
|
||||
return (
|
||||
STREAM_KINDS.has(kind) ||
|
||||
STREAM_MIME_PREFIXES.some((prefix) => mime?.startsWith(prefix))
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_CACHED = 200;
|
||||
|
||||
const ready = new Map<string, InlineMedia>();
|
||||
@@ -130,6 +141,26 @@ function cacheKey(account: number, chatId: number, messageId: number): string {
|
||||
return `${account}:${chatId}:${messageId}`;
|
||||
}
|
||||
|
||||
function forget(cache: Map<string, InlineMedia>, key: string) {
|
||||
const stale = cache.get(key);
|
||||
cache.delete(key);
|
||||
if (stale?.state === "ready") {
|
||||
URL.revokeObjectURL(stale.url);
|
||||
}
|
||||
}
|
||||
|
||||
export function forgetMedia(
|
||||
chatId: number,
|
||||
messageId: number,
|
||||
mediaId: number
|
||||
): void {
|
||||
const account = accounts.selectedId;
|
||||
if (account !== null) {
|
||||
forget(ready, cacheKey(account, chatId, messageId));
|
||||
forget(byId, `${account}:${mediaId}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolve(
|
||||
chatId: number,
|
||||
messageId: number
|
||||
|
||||
@@ -560,3 +560,55 @@ export interface WatchedBot {
|
||||
title: string | null;
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export type StorageGroupName = "video" | "file" | "voice" | "audio" | "other";
|
||||
|
||||
export interface StorageGroup {
|
||||
bytes: number;
|
||||
files: number;
|
||||
group: StorageGroupName;
|
||||
}
|
||||
|
||||
export interface StorageSummary {
|
||||
bytes: number;
|
||||
files: number;
|
||||
groups: StorageGroup[];
|
||||
heavy_bytes: number;
|
||||
heavy_files: number;
|
||||
heavy_threshold: number;
|
||||
trash_bytes: number;
|
||||
trash_files: number;
|
||||
}
|
||||
|
||||
export interface StorageFile {
|
||||
chat_id: number;
|
||||
date: string | null;
|
||||
file_name: string;
|
||||
file_size: number;
|
||||
kind: string;
|
||||
media_id: number;
|
||||
message_id: number;
|
||||
mime: string | null;
|
||||
refetchable: boolean;
|
||||
refs: number;
|
||||
}
|
||||
|
||||
export interface TrashItem {
|
||||
chat_id: number;
|
||||
file_name: string;
|
||||
file_size: number | null;
|
||||
kind: string;
|
||||
message_id: number;
|
||||
mime: string | null;
|
||||
purge_at: string;
|
||||
refetchable: boolean;
|
||||
storage_key: string;
|
||||
trashed_at: string;
|
||||
}
|
||||
|
||||
export interface TrashOutcome {
|
||||
blocked: number[];
|
||||
bytes: number;
|
||||
files: number;
|
||||
keys: string[];
|
||||
}
|
||||
Reference in new issue
Block a user