mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 12:06:38 +08:00
feat: scrape metadata after organize
This commit is contained in:
@@ -12,6 +12,7 @@ export interface OrganizeOverrides {
|
||||
transfer_mode?: string
|
||||
media_type?: string
|
||||
scan_after?: boolean
|
||||
scrape_after?: boolean
|
||||
library_id?: string
|
||||
dry_run?: boolean
|
||||
}
|
||||
@@ -76,6 +77,15 @@ export const toolsAPI = {
|
||||
removed: number
|
||||
error?: string
|
||||
}>
|
||||
scrapes?: Array<{
|
||||
library_id: string
|
||||
name: string
|
||||
path: string
|
||||
matched: number
|
||||
skipped?: boolean
|
||||
reason?: string
|
||||
error?: string
|
||||
}>
|
||||
}>(
|
||||
'/admin/organize/source',
|
||||
opts,
|
||||
|
||||
@@ -45,9 +45,19 @@ function formatScanSummary(scans: Array<{ name: string; added: number; updated:
|
||||
return ` · 扫描 ${ok.length}/${scans.length} 个库 · 新入库 ${added} · 更新 ${updated} · 访问 ${visited}`
|
||||
}
|
||||
|
||||
function formatScrapeSummary(scrapes: Array<{ name: string; matched: number; skipped?: boolean; reason?: string; error?: string }>): string {
|
||||
if (scrapes.length === 0) return ''
|
||||
const ok = scrapes.filter((scrape) => !scrape.error && !scrape.skipped)
|
||||
const skipped = scrapes.filter((scrape) => scrape.skipped).length
|
||||
const matched = ok.reduce((sum, scrape) => sum + (scrape.matched ?? 0), 0)
|
||||
if (ok.length === 0 && skipped > 0) return ` · 刮削跳过 ${skipped} 个库`
|
||||
return ` · 刮削 ${ok.length}/${scrapes.length} 个库 · 匹配 ${matched}${skipped ? ` · 跳过 ${skipped}` : ''}`
|
||||
}
|
||||
|
||||
type AutoOrganizeConfig = {
|
||||
enabled: string
|
||||
afterDownload: string
|
||||
scrapeAfter: string
|
||||
sourceDir: string
|
||||
targetDir: string
|
||||
transferMode: string
|
||||
@@ -57,6 +67,7 @@ type AutoOrganizeConfig = {
|
||||
const AUTO_ORGANIZE_DEFAULTS: AutoOrganizeConfig = {
|
||||
enabled: 'false',
|
||||
afterDownload: 'false',
|
||||
scrapeAfter: 'false',
|
||||
sourceDir: '',
|
||||
targetDir: '',
|
||||
transferMode: 'hardlink',
|
||||
@@ -66,6 +77,7 @@ const AUTO_ORGANIZE_DEFAULTS: AutoOrganizeConfig = {
|
||||
const AUTO_ORGANIZE_KEYS: Record<keyof AutoOrganizeConfig, string> = {
|
||||
enabled: 'organize.auto',
|
||||
afterDownload: 'organizer.auto_after_download',
|
||||
scrapeAfter: 'organize.scrape_after',
|
||||
sourceDir: 'organize.source_dir',
|
||||
targetDir: 'organize.target_dir',
|
||||
transferMode: 'organize.transfer_mode',
|
||||
@@ -83,6 +95,7 @@ function mergeAutoOrganizeSettings(rows: Setting[]): AutoOrganizeConfig {
|
||||
return {
|
||||
enabled: idx[AUTO_ORGANIZE_KEYS.enabled] ?? AUTO_ORGANIZE_DEFAULTS.enabled,
|
||||
afterDownload: idx[AUTO_ORGANIZE_KEYS.afterDownload] ?? AUTO_ORGANIZE_DEFAULTS.afterDownload,
|
||||
scrapeAfter: idx[AUTO_ORGANIZE_KEYS.scrapeAfter] ?? AUTO_ORGANIZE_DEFAULTS.scrapeAfter,
|
||||
sourceDir: idx[AUTO_ORGANIZE_KEYS.sourceDir] ?? AUTO_ORGANIZE_DEFAULTS.sourceDir,
|
||||
targetDir: idx[AUTO_ORGANIZE_KEYS.targetDir] ?? AUTO_ORGANIZE_DEFAULTS.targetDir,
|
||||
transferMode: idx[AUTO_ORGANIZE_KEYS.transferMode] ?? AUTO_ORGANIZE_DEFAULTS.transferMode,
|
||||
@@ -114,6 +127,7 @@ export function FileManagerPage() {
|
||||
const [organizeTransferMode, setOrganizeTransferMode] = useState('hardlink')
|
||||
const [organizeMediaType, setOrganizeMediaType] = useState('auto')
|
||||
const [scanAfter, setScanAfter] = useState(true)
|
||||
const [scrapeAfter, setScrapeAfter] = useState(false)
|
||||
const [organizeBusy, setOrganizeBusy] = useState('')
|
||||
const [previewItems, setPreviewItems] = useState<Array<{
|
||||
source: string
|
||||
@@ -160,7 +174,9 @@ export function FileManagerPage() {
|
||||
adminAPI
|
||||
.listSettings()
|
||||
.then((rows) => {
|
||||
setAutoConfig(mergeAutoOrganizeSettings(rows))
|
||||
const nextConfig = mergeAutoOrganizeSettings(rows)
|
||||
setAutoConfig(nextConfig)
|
||||
setScrapeAfter(settingOn(nextConfig.scrapeAfter))
|
||||
setAutoDirty(false)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
@@ -309,7 +325,7 @@ export function FileManagerPage() {
|
||||
if (!dryRun) {
|
||||
const ok = await confirmAction({
|
||||
title: '确认整理入库',
|
||||
message: `来源:${organizeSource}\n目标:${organizeDestPath}\n方式:${organizeTransferMode}${scanAfter ? '\n整理完成后会扫描入库。' : ''}`,
|
||||
message: `来源:${organizeSource}\n目标:${organizeDestPath}\n方式:${organizeTransferMode}${scanAfter ? '\n整理完成后会扫描入库。' : ''}${scanAfter && scrapeAfter ? '\n扫描后会自动刮削。' : ''}`,
|
||||
confirmText: '开始整理',
|
||||
})
|
||||
if (!ok) return
|
||||
@@ -322,6 +338,7 @@ export function FileManagerPage() {
|
||||
transfer_mode: organizeTransferMode,
|
||||
media_type: organizeMediaType === 'auto' ? undefined : organizeMediaType,
|
||||
scan_after: !dryRun && scanAfter,
|
||||
scrape_after: !dryRun && scanAfter && scrapeAfter,
|
||||
library_id: !dryRun && scanAfter && organizeLibraryID ? organizeLibraryID : undefined,
|
||||
dry_run: dryRun,
|
||||
})
|
||||
@@ -340,7 +357,8 @@ export function FileManagerPage() {
|
||||
return
|
||||
}
|
||||
const scanText = scanAfter ? formatScanSummary(result.scans ?? []) : ''
|
||||
toast.success(`整理完成:新增 ${result.organized} · 替换 ${replaced} · 跳过 ${result.skipped}${scanText}`)
|
||||
const scrapeText = scanAfter && scrapeAfter ? formatScrapeSummary(result.scrapes ?? []) : ''
|
||||
toast.success(`整理完成:新增 ${result.organized} · 替换 ${replaced} · 跳过 ${result.skipped}${scanText}${scrapeText}`)
|
||||
refresh()
|
||||
} catch (err: unknown) {
|
||||
toast.error((err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '整理失败')
|
||||
@@ -498,6 +516,14 @@ export function FileManagerPage() {
|
||||
/>
|
||||
qB 下载完成后自动整理
|
||||
</label>
|
||||
<label className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-2 py-1 text-xs text-ink-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settingOn(autoConfig.scrapeAfter)}
|
||||
onChange={(event) => changeAutoConfig('scrapeAfter', event.target.checked ? 'true' : 'false')}
|
||||
/>
|
||||
整理后自动刮削
|
||||
</label>
|
||||
<span className="text-xs text-sand-500">
|
||||
{autoDirty ? '有未保存设置' : '设置已同步'} · 定时任务名:organize_source
|
||||
</span>
|
||||
@@ -568,6 +594,15 @@ export function FileManagerPage() {
|
||||
<input type="checkbox" checked={scanAfter} onChange={(event) => setScanAfter(event.target.checked)} />
|
||||
整理后扫描入库
|
||||
</label>
|
||||
<label className="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50 px-2 py-1 text-xs text-ink-100">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={scanAfter && scrapeAfter}
|
||||
disabled={!scanAfter}
|
||||
onChange={(event) => setScrapeAfter(event.target.checked)}
|
||||
/>
|
||||
整理后自动刮削
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="neon-button !border-primary-400/30 !bg-white !text-brand-500"
|
||||
|
||||
@@ -160,6 +160,13 @@ const GROUPS: SettingGroup[] = [
|
||||
type: 'toggle',
|
||||
hint: '开启后 qB 下载完成时,系统会优先使用种子的 content_path 整理该文件/目录,并在整理完成后扫描目标媒体库。',
|
||||
},
|
||||
{
|
||||
key: 'organize.scrape_after',
|
||||
label: '整理后自动刮削',
|
||||
type: 'toggle',
|
||||
hint: '开启后,手动/自动整理完成并扫描入库后,会立即触发 TMDb/豆瓣/Bangumi/JavBus/JavDB 等元数据刮削。需要先配置可用刮削源。',
|
||||
defaultValue: 'false',
|
||||
},
|
||||
{
|
||||
key: 'downloads.smart_classify',
|
||||
label: '下载器智能分类',
|
||||
|
||||
Reference in New Issue
Block a user