Add custom download destinations, confirmations, and the bot error feed
This commit is contained in:
@@ -286,3 +286,37 @@
|
||||
background: var(--color-accent-400, #4f8cff);
|
||||
animation: vk-media-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.vk-media-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vk-media-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font: 400 11px/1.4 ui-monospace, Menlo, monospace;
|
||||
color: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.vk-media-dest {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vk-media-error {
|
||||
color: #ff8080;
|
||||
}
|
||||
|
||||
.vk-media-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 7px;
|
||||
background: #e5484d;
|
||||
color: #fff;
|
||||
font: 600 9px/1 ui-monospace, Menlo, monospace;
|
||||
}
|
||||
|
||||
+189
-21
@@ -22,6 +22,7 @@
|
||||
dms: [],
|
||||
loadedChannels: false,
|
||||
authors: null,
|
||||
errors: [],
|
||||
filters: {
|
||||
source: "",
|
||||
channelId: "",
|
||||
@@ -92,6 +93,13 @@
|
||||
document.querySelector("#scope-bot")?.classList.toggle("active", view === "bot");
|
||||
document.querySelector("#scope-guild")?.classList.remove("active");
|
||||
document.querySelector("#scope-dm")?.classList.remove("active");
|
||||
|
||||
if (view === "bot") {
|
||||
localStorage.setItem("vk-media-errors-seen", String(errorCount));
|
||||
updateErrorBadge();
|
||||
void refreshErrors();
|
||||
}
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -191,7 +199,7 @@
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
async function startScrape(channelId, guildId, mode) {
|
||||
async function startScrape(channelId, guildId, mode, downloadDir) {
|
||||
if (!channelId) {
|
||||
setStatus("no channel selected");
|
||||
return;
|
||||
@@ -202,7 +210,8 @@
|
||||
channelId,
|
||||
guildId: guildId ?? null,
|
||||
mode,
|
||||
backend: guildId ? "auto" : "puppet"
|
||||
backend: guildId ? "auto" : "puppet",
|
||||
downloadDir: downloadDir ?? null
|
||||
});
|
||||
setStatus(`started ${scrape.id} (${mode})`);
|
||||
scrapeModal.hidden = true;
|
||||
@@ -219,8 +228,8 @@
|
||||
function renderScrapes() {
|
||||
scrapeList.replaceChildren();
|
||||
const active = state.scrapes.filter(scrape => ["queued", "running", "paused"].includes(scrape.state));
|
||||
const rows = active.length ? active : state.scrapes.slice(0, 5);
|
||||
for (const scrape of rows) {
|
||||
|
||||
for (const scrape of active) {
|
||||
const row = el("div", "vk-media-scrape");
|
||||
const media = scrape.media ?? {};
|
||||
|
||||
@@ -239,21 +248,79 @@
|
||||
)
|
||||
);
|
||||
|
||||
if (active.includes(scrape)) {
|
||||
row.append(
|
||||
button("Stop", async () => {
|
||||
await window.VK.api.post("/api/media/scrape/stop", { id: scrape.id });
|
||||
void loadScrapes();
|
||||
})
|
||||
);
|
||||
} else {
|
||||
row.append(el("span", "vk-media-status", durationLabel(scrape)));
|
||||
}
|
||||
row.append(
|
||||
button("Stop", async () => {
|
||||
await window.VK.api.post("/api/media/scrape/stop", { id: scrape.id });
|
||||
void loadScrapes();
|
||||
})
|
||||
);
|
||||
|
||||
scrapeList.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Bot errors ----
|
||||
|
||||
let errorCount = 0;
|
||||
|
||||
function seenErrorCount() {
|
||||
return Number(localStorage.getItem("vk-media-errors-seen") ?? 0);
|
||||
}
|
||||
|
||||
function updateErrorBadge() {
|
||||
const badge = document.querySelector("#scope-bot-badge");
|
||||
if (!badge) return;
|
||||
|
||||
const unseen = Math.max(0, errorCount - seenErrorCount());
|
||||
badge.textContent = String(unseen);
|
||||
badge.hidden = unseen === 0;
|
||||
}
|
||||
|
||||
function renderErrors() {
|
||||
scrapeList.replaceChildren();
|
||||
|
||||
const head = el("div", "vk-media-scrape");
|
||||
head.append(el("span", null, `${(state.errors ?? []).length} error(s)`));
|
||||
head.append(
|
||||
button("Clear errors", async () => {
|
||||
try {
|
||||
await window.VK.api.post("/api/media/errors/clear", {});
|
||||
} catch {
|
||||
// the list stays until the next refresh
|
||||
}
|
||||
state.errors = [];
|
||||
errorCount = 0;
|
||||
localStorage.setItem("vk-media-errors-seen", "0");
|
||||
updateErrorBadge();
|
||||
renderErrors();
|
||||
})
|
||||
);
|
||||
scrapeList.append(head);
|
||||
|
||||
for (const entry of state.errors ?? []) {
|
||||
scrapeList.append(
|
||||
el(
|
||||
"div",
|
||||
"vk-media-scrape vk-media-error",
|
||||
`${new Date(entry.at).toLocaleTimeString()} · ${entry.source} · ${entry.message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!(state.errors ?? []).length) scrapeList.append(el("div", "vk-media-scrape", "No errors."));
|
||||
}
|
||||
|
||||
async function refreshErrors() {
|
||||
try {
|
||||
state.errors = await window.VK.api.get("/api/media/errors");
|
||||
} catch {
|
||||
state.errors = [];
|
||||
}
|
||||
errorCount = (state.errors ?? []).length;
|
||||
updateErrorBadge();
|
||||
if (state.view === "bot") renderErrors();
|
||||
}
|
||||
|
||||
function renderGrid() {
|
||||
grid.replaceChildren();
|
||||
for (const item of state.items) {
|
||||
@@ -319,8 +386,7 @@
|
||||
toolbar.append(chip);
|
||||
}
|
||||
|
||||
toolbar.append(button("New scrape…", openScrapeModal));
|
||||
toolbar.append(button("History", openHistoryModal));
|
||||
|
||||
|
||||
const sourceSelect = document.createElement("select");
|
||||
sourceSelect.className = "vk-chip vk-chip-off";
|
||||
@@ -428,6 +494,10 @@
|
||||
void loadMedia(true);
|
||||
})
|
||||
);
|
||||
|
||||
toolbar.append(el("span", "vk-media-spacer"));
|
||||
toolbar.append(button("History", openHistoryModal));
|
||||
toolbar.append(button("New scrape…", openScrapeModal));
|
||||
} else {
|
||||
toolbar.append(el("span", null, "Slopbot is the dedicated bot core. Setup: docs/slopbot.md."));
|
||||
}
|
||||
@@ -444,12 +514,14 @@
|
||||
statusLine.textContent = bot?.connected
|
||||
? `connected as ${bot.user} in ${bot.guilds.length} guild(s)`
|
||||
: "slopbot is disabled or not logged in (VESKSLOP_SLOPBOT_ENABLED=1 and a token)";
|
||||
void loadScrapes().then(renderScrapes);
|
||||
void refreshErrors();
|
||||
} else {
|
||||
void loadChannels().then(renderToolbar);
|
||||
void loadAuthors().then(renderToolbar);
|
||||
void loadMedia(true);
|
||||
void loadScrapes();
|
||||
}
|
||||
void loadScrapes();
|
||||
}
|
||||
|
||||
overlay.querySelector("#media-close").addEventListener("click", hide);
|
||||
@@ -465,6 +537,16 @@
|
||||
document.querySelector("#scope-guild")?.addEventListener("click", hide);
|
||||
document.querySelector("#scope-dm")?.addEventListener("click", hide);
|
||||
|
||||
const errorBadge = el("span", "vk-media-badge");
|
||||
errorBadge.id = "scope-bot-badge";
|
||||
errorBadge.hidden = true;
|
||||
scopeBot?.append(errorBadge);
|
||||
|
||||
void refreshErrors();
|
||||
setInterval(() => {
|
||||
if (state.view !== "bot") void refreshErrors();
|
||||
}, 15000);
|
||||
|
||||
// ---- Scrape popup: server or group-chat trees ----
|
||||
|
||||
const scrapeModal = document.createElement("div");
|
||||
@@ -472,7 +554,11 @@
|
||||
scrapeModal.hidden = true;
|
||||
scrapeModal.innerHTML = `
|
||||
<div class="vk-media-modal-box vk-media-wide">
|
||||
<strong>New scrape</strong>
|
||||
<div class="vk-media-head">
|
||||
<strong>New scrape</strong>
|
||||
<span class="vk-media-spacer"></span>
|
||||
<button id="scrape-x" class="btn btn-secondary vk-small-btn" type="button">X</button>
|
||||
</div>
|
||||
<div class="vk-media-toolbar">
|
||||
<button id="scrape-tab-server" class="vk-chip vk-chip-on" type="button">Server</button>
|
||||
<button id="scrape-tab-group" class="vk-chip vk-chip-off" type="button">Group chat</button>
|
||||
@@ -491,8 +577,8 @@
|
||||
function channelRow(channelId, guildId, label) {
|
||||
const row = el("div", "vk-media-channel");
|
||||
row.append(el("span", "vk-media-channel-name", label));
|
||||
row.append(button("Links", () => void startScrape(channelId, guildId, "links")));
|
||||
row.append(button("Download", () => void startScrape(channelId, guildId, "download")));
|
||||
row.append(button("Links", () => askScrape(channelId, guildId, "links", label)));
|
||||
row.append(button("Download", () => askScrape(channelId, guildId, "download", label)));
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -558,6 +644,78 @@
|
||||
scrapeModal.querySelector("#scrape-cancel").addEventListener("click", () => {
|
||||
scrapeModal.hidden = true;
|
||||
});
|
||||
scrapeModal.querySelector("#scrape-x").addEventListener("click", () => {
|
||||
scrapeModal.hidden = true;
|
||||
});
|
||||
scrapeModal.addEventListener("click", event => {
|
||||
if (event.target === scrapeModal) scrapeModal.hidden = true;
|
||||
});
|
||||
|
||||
// ---- Confirmation popup for Links / Download ----
|
||||
|
||||
const confirmModal = document.createElement("div");
|
||||
confirmModal.className = "vk-media-modal";
|
||||
confirmModal.hidden = true;
|
||||
confirmModal.innerHTML = `
|
||||
<div class="vk-media-modal-box">
|
||||
<strong id="confirm-title">Start scrape?</strong>
|
||||
<div id="confirm-note" class="vk-media-status"></div>
|
||||
<label id="confirm-dest-row" class="vk-media-check" hidden>
|
||||
<input id="confirm-custom-dest" type="checkbox" /> Custom download destination
|
||||
</label>
|
||||
<input
|
||||
id="confirm-dest"
|
||||
class="vk-media-input vk-media-dest"
|
||||
type="text"
|
||||
placeholder="/path/to/download/"
|
||||
hidden
|
||||
/>
|
||||
<div class="vk-media-toolbar">
|
||||
<button id="confirm-cancel" class="btn btn-secondary vk-small-btn" type="button">Cancel</button>
|
||||
<button id="confirm-start" class="btn btn-primary vk-small-btn" type="button">Start</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.append(confirmModal);
|
||||
|
||||
let confirmAction = null;
|
||||
|
||||
function askScrape(channelId, guildId, mode, label) {
|
||||
const isDownload = mode === "download";
|
||||
const destRow = confirmModal.querySelector("#confirm-dest-row");
|
||||
const destInput = confirmModal.querySelector("#confirm-dest");
|
||||
const checkbox = confirmModal.querySelector("#confirm-custom-dest");
|
||||
|
||||
confirmModal.querySelector("#confirm-title").textContent =
|
||||
isDownload ? "Download scrape?" : "Links scrape?";
|
||||
confirmModal.querySelector("#confirm-note").textContent = isDownload
|
||||
? `${label} — downloads files${guildId ? "" : " with the puppet"}.`
|
||||
: `${label} — indexes links and caches previews${guildId ? "" : " with the puppet"}.`;
|
||||
|
||||
destRow.hidden = !isDownload;
|
||||
destInput.hidden = true;
|
||||
destInput.value = "";
|
||||
checkbox.checked = false;
|
||||
checkbox.onchange = () => {
|
||||
destInput.hidden = !checkbox.checked;
|
||||
};
|
||||
|
||||
confirmAction = () =>
|
||||
startScrape(channelId, guildId, mode, isDownload && checkbox.checked ? destInput.value.trim() : null);
|
||||
confirmModal.hidden = false;
|
||||
}
|
||||
|
||||
confirmModal.querySelector("#confirm-cancel").addEventListener("click", () => {
|
||||
confirmModal.hidden = true;
|
||||
});
|
||||
confirmModal.querySelector("#confirm-start").addEventListener("click", () => {
|
||||
const action = confirmAction;
|
||||
confirmModal.hidden = true;
|
||||
void action?.();
|
||||
});
|
||||
confirmModal.addEventListener("click", event => {
|
||||
if (event.target === confirmModal) confirmModal.hidden = true;
|
||||
});
|
||||
|
||||
// ---- History popup ----
|
||||
|
||||
@@ -566,7 +724,11 @@
|
||||
historyModal.hidden = true;
|
||||
historyModal.innerHTML = `
|
||||
<div class="vk-media-modal-box vk-media-wide">
|
||||
<strong>Scrape history</strong>
|
||||
<div class="vk-media-head">
|
||||
<strong>Scrape history</strong>
|
||||
<span class="vk-media-spacer"></span>
|
||||
<button id="history-x" class="btn btn-secondary vk-small-btn" type="button">X</button>
|
||||
</div>
|
||||
<div id="history-list" class="vk-media-modal-list"></div>
|
||||
<div class="vk-media-toolbar">
|
||||
<button id="history-close" class="btn btn-secondary vk-small-btn" type="button">Close</button>
|
||||
@@ -630,6 +792,12 @@
|
||||
historyModal.querySelector("#history-close").addEventListener("click", () => {
|
||||
historyModal.hidden = true;
|
||||
});
|
||||
historyModal.querySelector("#history-x").addEventListener("click", () => {
|
||||
historyModal.hidden = true;
|
||||
});
|
||||
historyModal.addEventListener("click", event => {
|
||||
if (event.target === historyModal) historyModal.hidden = true;
|
||||
});
|
||||
|
||||
// ---- Settings: media storage ----
|
||||
|
||||
|
||||
@@ -774,11 +774,24 @@ function buildServer(): McpServer {
|
||||
.enum(["auto", "slopbot", "puppet"])
|
||||
.optional()
|
||||
.describe("auto (default) picks slopbot for its guilds, the client otherwise"),
|
||||
confirm: z.boolean().optional().describe("Accept the near-limit warning")
|
||||
confirm: z.boolean().optional().describe("Accept the near-limit warning"),
|
||||
downloadDir: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Absolute destination for downloads; previews stay local")
|
||||
}
|
||||
},
|
||||
async ({ channelId, guildId, mode, backend, confirm }) =>
|
||||
jsonText(await media.startScrape({ channelId, guildId: guildId ?? null, mode, backend, confirm }))
|
||||
async ({ channelId, guildId, mode, backend, confirm, downloadDir }) =>
|
||||
jsonText(
|
||||
await media.startScrape({
|
||||
channelId,
|
||||
guildId: guildId ?? null,
|
||||
mode,
|
||||
backend,
|
||||
confirm,
|
||||
downloadDir: downloadDir ?? null
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
|
||||
+71
-22
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import {
|
||||
dirSizeBytes,
|
||||
isNearLimit,
|
||||
@@ -49,6 +49,14 @@ export interface StartScrapeInput {
|
||||
backend?: "auto" | "slopbot" | "puppet";
|
||||
/** Set by a re-run of the command after the near-limit warning. */
|
||||
confirm?: boolean;
|
||||
/** Absolute destination for downloaded files. Null keeps them under the scrape dir. */
|
||||
downloadDir?: string | null;
|
||||
}
|
||||
|
||||
export interface MediaErrorEntry {
|
||||
at: string;
|
||||
source: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ScrapeBytes {
|
||||
@@ -138,6 +146,9 @@ export class MediaService {
|
||||
private botFetcher: SlopbotFetcher | null = null;
|
||||
private botStarting: Promise<void> | null = null;
|
||||
|
||||
/** The newest bot and scrape errors, capped. Surfaced in the Bot tab. */
|
||||
private readonly errors: MediaErrorEntry[] = [];
|
||||
|
||||
constructor(private readonly deps: MediaServiceDeps) {
|
||||
const number = (value: string | undefined, fallback: number) => {
|
||||
const parsed = Number(value);
|
||||
@@ -147,6 +158,7 @@ export class MediaService {
|
||||
this.engine = new ScrapeEngine({
|
||||
rootDir: this.rootDir,
|
||||
log: line => this.deps.log?.(line),
|
||||
onError: (scrapeId, message) => this.recordError("scrape", `${scrapeId}: ${message}`),
|
||||
download: {
|
||||
maxFileBytes: number(process.env.VESKSLOP_MEDIA_MAX_FILE_BYTES, 50 * 1024 * 1024),
|
||||
previewMaxBytes: number(process.env.VESKSLOP_MEDIA_PREVIEW_MAX_BYTES, 5 * 1024 * 1024),
|
||||
@@ -171,7 +183,9 @@ export class MediaService {
|
||||
this.deps.log?.("slopbot connected");
|
||||
})
|
||||
.catch(error => {
|
||||
this.deps.log?.(`slopbot login failed: ${String(error)}`);
|
||||
const message = `slopbot login failed: ${String(error)}`;
|
||||
this.deps.log?.(message);
|
||||
this.recordError("slopbot", message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,6 +278,10 @@ export class MediaService {
|
||||
}
|
||||
|
||||
async startScrape(input: StartScrapeInput): Promise<ScrapeMeta> {
|
||||
if (input.downloadDir && !isAbsolute(input.downloadDir)) {
|
||||
throw new Error("downloadDir must be an absolute path");
|
||||
}
|
||||
|
||||
this.applyRetention();
|
||||
|
||||
const status = this.storageStatus();
|
||||
@@ -275,30 +293,38 @@ export class MediaService {
|
||||
);
|
||||
}
|
||||
|
||||
const mode = input.mode ?? "links";
|
||||
let backend: ScrapeBackend;
|
||||
try {
|
||||
const mode = input.mode ?? "links";
|
||||
let backend: ScrapeBackend;
|
||||
|
||||
if (input.backend === "puppet") {
|
||||
backend = this.puppetBackend();
|
||||
} else {
|
||||
const available = await this.ensureSlopbot();
|
||||
const inGuild = !!input.guildId && (this.bot?.guilds.cache.has(input.guildId) ?? false);
|
||||
if (input.backend === "puppet") {
|
||||
backend = this.puppetBackend();
|
||||
} else {
|
||||
const available = await this.ensureSlopbot();
|
||||
const inGuild = !!input.guildId && (this.bot?.guilds.cache.has(input.guildId) ?? false);
|
||||
|
||||
if (input.backend === "slopbot" && (!available || !inGuild)) {
|
||||
throw new Error("slopbot is not connected to that guild");
|
||||
if (input.backend === "slopbot" && (!available || !inGuild)) {
|
||||
throw new Error("slopbot is not connected to that guild");
|
||||
}
|
||||
|
||||
const fetcher = this.botFetcher;
|
||||
backend =
|
||||
available && inGuild && fetcher
|
||||
? new SlopbotBackend(fetcher)
|
||||
: this.puppetBackend();
|
||||
}
|
||||
|
||||
const fetcher = this.botFetcher;
|
||||
backend =
|
||||
available && inGuild && fetcher ? new SlopbotBackend(fetcher) : this.puppetBackend();
|
||||
return this.engine.start({
|
||||
channelId: input.channelId,
|
||||
guildId: input.guildId ?? null,
|
||||
mode,
|
||||
backend,
|
||||
downloadDir: input.downloadDir ?? null
|
||||
});
|
||||
} catch (error) {
|
||||
this.recordError("scrape", error instanceof Error ? error.message : String(error));
|
||||
throw error;
|
||||
}
|
||||
|
||||
return this.engine.start({
|
||||
channelId: input.channelId,
|
||||
guildId: input.guildId ?? null,
|
||||
mode,
|
||||
backend
|
||||
});
|
||||
}
|
||||
|
||||
stopScrape(id: string): void {
|
||||
@@ -322,6 +348,10 @@ export class MediaService {
|
||||
|
||||
/** Absolute path and content type for a downloaded file, or null. */
|
||||
getMediaFile(scrapeId: string, itemId: number): { path: string; contentType: string } | null {
|
||||
// A custom destination is the user's filesystem, outside the archive: not served.
|
||||
const meta = this.engine.get(scrapeId);
|
||||
if (meta?.downloadDir) return null;
|
||||
|
||||
const item = this.getMediaItem(scrapeId, itemId);
|
||||
if (!item?.file_path) return null;
|
||||
|
||||
@@ -343,6 +373,22 @@ export class MediaService {
|
||||
};
|
||||
}
|
||||
|
||||
private recordError(source: string, message: string): void {
|
||||
this.errors.unshift({ at: new Date().toISOString(), source, message });
|
||||
if (this.errors.length > 50) this.errors.length = 50;
|
||||
}
|
||||
|
||||
/** Bot and scrape errors, newest first. */
|
||||
listErrors(): MediaErrorEntry[] {
|
||||
return [...this.errors];
|
||||
}
|
||||
|
||||
clearErrors(): number {
|
||||
const count = this.errors.length;
|
||||
this.errors.length = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
/** Distinct posters across every scrape, biggest first. */
|
||||
listAuthors(): Array<{ id: string | null; name: string | null; count: number }> {
|
||||
const totals = new Map<string, { id: string | null; name: string | null; count: number }>();
|
||||
@@ -434,7 +480,10 @@ export class MediaService {
|
||||
height: row.height,
|
||||
aspectRatio: row.aspect_ratio,
|
||||
url: row.url,
|
||||
localUrl: row.file_path ? `/api/media/${meta.id}/${row.id}/file` : null,
|
||||
localUrl:
|
||||
row.file_path && !meta.downloadDir
|
||||
? `/api/media/${meta.id}/${row.id}/file`
|
||||
: null,
|
||||
thumbUrl: row.preview_hash ? `/api/media/${meta.id}/${row.id}/thumb` : null,
|
||||
provider: row.provider,
|
||||
downloaded: row.downloaded === 1,
|
||||
|
||||
+23
-1
@@ -832,6 +832,20 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
|
||||
})
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/api/media/errors",
|
||||
wrap(async (_req, res) => {
|
||||
res.json({ ok: true, result: deps.media.listErrors() });
|
||||
})
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/api/media/errors/clear",
|
||||
wrap(async (_req, res) => {
|
||||
res.json({ ok: true, result: { cleared: deps.media.clearErrors() } });
|
||||
})
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/api/media/scrape",
|
||||
wrap(async (req, res) => {
|
||||
@@ -844,8 +858,16 @@ export function startWebUi(deps: WebUiDeps): WebUiHandle {
|
||||
const requested = String(req.body?.backend ?? "auto");
|
||||
const backend =
|
||||
requested === "slopbot" || requested === "puppet" ? requested : "auto";
|
||||
const downloadDir = req.body?.downloadDir ? String(req.body.downloadDir) : null;
|
||||
|
||||
const scrape = await deps.media.startScrape({ channelId, guildId, mode, confirm, backend });
|
||||
const scrape = await deps.media.startScrape({
|
||||
channelId,
|
||||
guildId,
|
||||
mode,
|
||||
confirm,
|
||||
backend,
|
||||
downloadDir
|
||||
});
|
||||
deps.getPuppet()?.appendAction({
|
||||
type: "media",
|
||||
action: "scrape",
|
||||
|
||||
Reference in New Issue
Block a user