From e3dbaaeb3102e0175e2d4c1aaf0ad6437f24f732 Mon Sep 17 00:00:00 2001 From: noelorin Date: Sun, 27 Sep 2026 18:33:13 +0800 Subject: [PATCH] feat(settings): auto-scrape toggle default off Site setting auto_scrape; scan/ensurePath skip Bangumi when off; admin switch on /settings. --- .../components/pages/SettingsAdminBody.tsx | 57 +++++++++++++++++++ packages/i18n/src/messages/en.ts | 5 ++ packages/i18n/src/messages/zh-CN.ts | 5 ++ packages/models/src/app-settings.ts | 2 + packages/models/src/index.ts | 1 + packages/trpc/src/router/settings.router.ts | 8 +++ packages/trpc/src/services/library.service.ts | 9 ++- .../trpc/src/services/settings.service.ts | 27 +++++++++ packages/types/src/schemas.ts | 6 ++ 9 files changed, 118 insertions(+), 2 deletions(-) diff --git a/app/web/src/components/pages/SettingsAdminBody.tsx b/app/web/src/components/pages/SettingsAdminBody.tsx index bed3513..62d010a 100644 --- a/app/web/src/components/pages/SettingsAdminBody.tsx +++ b/app/web/src/components/pages/SettingsAdminBody.tsx @@ -4,6 +4,7 @@ import { Button, Card, Input, toast } from "@app/ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { createTRPCReactClient, trpc } from "~/lib/trpc"; +import { cn } from "~/lib/utils"; function Inner(props: { locale: Locale }) { const { locale } = props; @@ -11,6 +12,8 @@ function Inner(props: { locale: Locale }) { const isAdmin = me.data?.user?.role === "admin"; const openNet = trpc.settings.getOpenNetwork.useQuery(undefined, { enabled: isAdmin }); const saveOpenNet = trpc.settings.saveOpenNetwork.useMutation(); + const autoScrapeQ = trpc.settings.getAutoScrape.useQuery(undefined, { enabled: isAdmin }); + const autoScrapeSave = trpc.settings.saveAutoScrape.useMutation(); const ddpStatus = trpc.settings.dandanplayAuthStatus.useQuery(undefined, { enabled: Boolean(me.data?.user), }); @@ -18,12 +21,18 @@ function Inner(props: { locale: Locale }) { const ddpLogout = trpc.settings.logoutDandanplay.useMutation(); const [ddpUser, setDdpUser] = useState(""); const [ddpPass, setDdpPass] = useState(""); + const [autoScrapeOn, setAutoScrapeOn] = useState(false); + const [autoFilled, setAutoFilled] = useState(false); const [appId, setAppId] = useState(""); const [appSecret, setAppSecret] = useState(""); const [filled, setFilled] = useState(false); useEffect(() => { + if (autoScrapeQ.data !== undefined && !autoFilled) { + setAutoScrapeOn(autoScrapeQ.data); + setAutoFilled(true); + } if (!openNet.data || filled) return; setAppId(openNet.data.appId); setAppSecret(openNet.data.appSecret); @@ -188,6 +197,54 @@ function Inner(props: { locale: Locale }) { )} + +
+
+

{label("settings.autoScrape")}

+ + {autoScrapeOn + ? label("settings.autoScrapeOn") + : label("settings.autoScrapeOff")} + +
+

+ {label("settings.autoScrapeHint")} +

+ + +
); } diff --git a/packages/i18n/src/messages/en.ts b/packages/i18n/src/messages/en.ts index 5cb5447..1eb3deb 100644 --- a/packages/i18n/src/messages/en.ts +++ b/packages/i18n/src/messages/en.ts @@ -359,4 +359,9 @@ export const en: Record = { "watch.clearLocalOk": "Local danmaku cleared", "settings.nonAdminHint": "Admin configures the open network; any signed-in user can bind the shared DanDanPlay session here.", "settings.sharedAccountHint": "The DanDanPlay account is a site-wide session used to fetch online danmaku.", + "settings.autoScrape": "Auto scrape", + "settings.autoScrapeHint": "Automatically match Bangumi metadata on scan/import. Off by default to avoid external API calls.", + "settings.autoScrapeOn": "Auto scrape enabled", + "settings.autoScrapeOff": "Off (default)", + "settings.autoScrapeSaved": "Saved", }; diff --git a/packages/i18n/src/messages/zh-CN.ts b/packages/i18n/src/messages/zh-CN.ts index 8b11664..1eda0a2 100644 --- a/packages/i18n/src/messages/zh-CN.ts +++ b/packages/i18n/src/messages/zh-CN.ts @@ -349,6 +349,11 @@ export const zhCN = { "watch.clearLocalOk": "已清除本地弹幕", "settings.nonAdminHint": "在线弹幕需管理员配置开放网络;普通账号可在此登录弹弹play 会话。", "settings.sharedAccountHint": "弹弹play 账号为站点共用会话,用于拉取在线弹幕。", + "settings.autoScrape": "自动刮削", + "settings.autoScrapeHint": "扫描/入库时自动匹配 Bangumi 元数据。默认关闭,避免自动请求外部接口。", + "settings.autoScrapeOn": "开启自动刮削", + "settings.autoScrapeOff": "已关闭(默认)", + "settings.autoScrapeSaved": "已保存", } as const; export type MessageKey = keyof typeof zhCN; diff --git a/packages/models/src/app-settings.ts b/packages/models/src/app-settings.ts index 0ec85f2..9557940 100644 --- a/packages/models/src/app-settings.ts +++ b/packages/models/src/app-settings.ts @@ -36,3 +36,5 @@ export const APP_SETTING_OPEN_DANMAKU_APP_SECRET = "open_danmaku_app_secret"; export const APP_SETTING_DANDANPLAY_TOKEN = "dandanplay_token"; export const APP_SETTING_DANDANPLAY_SCREEN_NAME = "dandanplay_screen_name"; + +export const APP_SETTING_AUTO_SCRAPE = "auto_scrape"; diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 9a2118a..97297d8 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -23,5 +23,6 @@ export { APP_SETTING_OPEN_DANMAKU_APP_SECRET, APP_SETTING_DANDANPLAY_TOKEN, APP_SETTING_DANDANPLAY_SCREEN_NAME, + APP_SETTING_AUTO_SCRAPE, } from "./app-settings.js"; export type { AppSettingRow, NewAppSettingRow } from "./app-settings.js"; diff --git a/packages/trpc/src/router/settings.router.ts b/packages/trpc/src/router/settings.router.ts index a20e4a6..8065c01 100644 --- a/packages/trpc/src/router/settings.router.ts +++ b/packages/trpc/src/router/settings.router.ts @@ -19,6 +19,14 @@ export const settingsRouter = t.router({ logoutDandanplay: protectedProcedure .input(settingsSchemas.logoutDandanplay) .mutation(({ ctx }) => settingsService.logoutDandanplay(ctx.userId)), + getAutoScrape: adminProcedure + .input(settingsSchemas.getAutoScrape) + .query(() => settingsService.getAutoScrapeEnabled()), + saveAutoScrape: adminProcedure + .input(settingsSchemas.saveAutoScrape) + .mutation(({ ctx, input }) => + settingsService.saveAutoScrape(ctx.userId, input.enabled), + ), dandanplayAuthStatus: protectedProcedure .input(settingsSchemas.logoutDandanplay) .query(() => settingsService.getDandanplayAuthStatus()), diff --git a/packages/trpc/src/services/library.service.ts b/packages/trpc/src/services/library.service.ts index 0ee139a..83eec2d 100644 --- a/packages/trpc/src/services/library.service.ts +++ b/packages/trpc/src/services/library.service.ts @@ -4,6 +4,7 @@ import { parseEpisodeFromFilename, type LibraryGroupPage } from "@app/types"; import { decryptSecret } from "./secret.js"; import { createWebdav, isVideoFilename, joinWebdavPath, listDirectory } from "./webdav-client.js"; import { scrapeService } from "./scrape.service.js"; +import { getAutoScrapeEnabled } from "./settings.service.js"; const MAX_SCAN_FILES = 2000; /** 同时执行的 upsert+刮削 job 上限,避免最坏 2000 并发打 Bangumi 被限流 */ @@ -53,7 +54,9 @@ async function walkVideos( title, size: entry.size, }); - await scrapeService.autoScrape(userId, item.id); + if (await getAutoScrapeEnabled()) { + await scrapeService.autoScrape(userId, item.id); + } }); } } @@ -251,7 +254,9 @@ export const libraryService = { title: basename.replace(/\.[a-z0-9]+$/i, ""), size, }); - await scrapeService.autoScrape(userId, item.id); + if (await getAutoScrapeEnabled()) { + await scrapeService.autoScrape(userId, item.id); + } return item; }, }; diff --git a/packages/trpc/src/services/settings.service.ts b/packages/trpc/src/services/settings.service.ts index e6bdad0..4a7e15a 100644 --- a/packages/trpc/src/services/settings.service.ts +++ b/packages/trpc/src/services/settings.service.ts @@ -1,4 +1,5 @@ import { + APP_SETTING_AUTO_SCRAPE, APP_SETTING_DANDANPLAY_SCREEN_NAME, APP_SETTING_DANDANPLAY_TOKEN, APP_SETTING_OPEN_DANMAKU_APP_ID, @@ -97,6 +98,8 @@ export const settingsService = { logoutDandanplay, getDandanplayAuthStatus, applyRuntimeSession, + getAutoScrapeEnabled, + saveAutoScrape, }; const APP_KEY_DDP_TOKEN = "dandanplay_token"; @@ -165,3 +168,27 @@ export async function applyRuntimeSession(): Promise { setSessionToken(null); } } + +/** 自动刮削开关:未配置默认关闭 */ +export async function getAutoScrapeEnabled(): Promise { + try { + const row = await appSettingsDao.get(APP_SETTING_AUTO_SCRAPE); + return row?.value === "1"; + } catch { + return false; + } +} + +export async function saveAutoScrape( + userId: string, + enabled: boolean, +): Promise<{ enabled: boolean }> { + await appSettingsDao.upsert({ + key: APP_SETTING_AUTO_SCRAPE, + value: enabled ? "1" : "0", + updatedBy: userId, + }); + return { enabled }; +} + +export const isAutoScrapeEnabled = getAutoScrapeEnabled; diff --git a/packages/types/src/schemas.ts b/packages/types/src/schemas.ts index c94dc64..002efef 100644 --- a/packages/types/src/schemas.ts +++ b/packages/types/src/schemas.ts @@ -153,6 +153,8 @@ export const settingsSchemas = { password: z.string().min(1, "请输入密码").max(128), }), logoutDandanplay: z.object({}).optional(), + getAutoScrape: z.object({}).optional(), + saveAutoScrape: z.object({ enabled: z.boolean() }), }; export type DandanplayLoginOutput = { @@ -175,3 +177,7 @@ export type SaveOpenNetworkSettingsInput = { appId: string; appSecret: string; }; + +export type AutoScrapeSettingsOutput = { + enabled: boolean; +};