Add media storage limits, retention, and the near-limit flow

This commit is contained in:
Fisto
2026-09-16 08:49:42 +02:00
parent 6b961ddcef
commit 8f3f153c55
15 changed files with 394 additions and 12 deletions
+46 -3
View File
@@ -20,7 +20,7 @@ import type { AuthService, AuthStatus } from "./auth-service.js";
import type { AudioService } from "./audio-service.js";
import { CoreError, type CoreClient } from "./core-client.js";
import type { CoreProcess } from "./core-process.js";
import type { MediaService } from "./media.js";
import { MediaLimitError, type MediaService } from "./media.js";
import { dedupe, getQueryCount, paced, pacedSend, pacedVoice } from "./pacing.js";
import type { PreferencesStore, WebPreferences } from "./preferences.js";
import type { Puppet } from "./puppet.js";
@@ -206,6 +206,16 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
return;
}
if (error instanceof MediaLimitError) {
res.status(409).json({
ok: false,
code: error.code,
error: error.message,
result: { usedBytes: error.usedBytes, limitBytes: error.limitBytes }
});
return;
}
const status = error instanceof HttpError ? error.status : 500;
res.status(status).json({ ok: false, error: String(error?.message ?? error) });
});
@@ -403,7 +413,7 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
app.post(
"/api/preferences",
wrap(async (req, res) => {
const { visibleGuilds, tts, audio, favourites } = req.body ?? {};
const { visibleGuilds, tts, audio, favourites, media: mediaPrefs } = req.body ?? {};
if (visibleGuilds !== undefined && visibleGuilds !== null && !Array.isArray(visibleGuilds)) {
throw new HttpError(400, "visibleGuilds must be an array of guild IDs or null");
@@ -438,6 +448,21 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
};
}
if (mediaPrefs !== undefined) {
const current = deps.preferences.get().media;
patch.media = {
limitBytes: Number.isFinite(Number(mediaPrefs?.limitBytes))
? Number(mediaPrefs.limitBytes)
: current.limitBytes,
warnPercent: Number.isFinite(Number(mediaPrefs?.warnPercent))
? Number(mediaPrefs.warnPercent)
: current.warnPercent,
overrunPercent: Number.isFinite(Number(mediaPrefs?.overrunPercent))
? Number(mediaPrefs.overrunPercent)
: current.overrunPercent
};
}
if (audio !== undefined) {
patch.audio = { duck: audio?.duck !== false };
deps.audio.setDuckEnabled(patch.audio.duck);
@@ -729,6 +754,23 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
})
);
app.get(
"/api/media/storage",
wrap(async (_req, res) => {
res.json({ ok: true, result: deps.media.refreshRetention() });
})
);
app.post(
"/api/media/clean",
wrap(async (req, res) => {
const ids = Array.isArray(req.body?.ids) ? req.body.ids.map(String) : undefined;
const deleted = deps.media.cleanMedia(ids);
deps.getPuppet()?.appendAction({ type: "media", action: "clean", deleted });
res.json({ ok: true, result: { deleted } });
})
);
app.get(
"/api/media",
wrap(async (req, res) => {
@@ -750,8 +792,9 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
const mode = req.body?.mode === "download" ? "download" : "links";
const guildId = req.body?.guildId ? String(req.body.guildId) : null;
const confirm = req.body?.confirm === true;
const scrape = await deps.media.startScrape({ channelId, guildId, mode });
const scrape = await deps.media.startScrape({ channelId, guildId, mode, confirm });
deps.getPuppet()?.appendAction({
type: "media",
action: "scrape",