feat(frontend,backend): storage panel with weight rows and trash, heavy media streams by url in the viewer

This commit is contained in:
hh committed 2026-10-14 03:09:37 +02:00
1 parent f2c245707a
commit 55634984fd
19 files changed
+1822 -24

No files matched your search

+13
View File
@@ -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" }
+44
View File
@@ -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",
});
}
+31
View File
@@ -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
+52
View File
@@ -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[];
}