import { useEffect, useState } from 'react' import toast from 'react-hot-toast' import { Copy, Trash2 } from 'lucide-react' import { duplicatesAPI, type DuplicateReport } from '../api/duplicates' import { libraryAPI } from '../api/library' import { confirmAction } from '../components/confirmAction' import type { Library } from '../types' function fmtBytes(n: number): string { if (!n) return '0 B' const u = ['B', 'KB', 'MB', 'GB', 'TB'] let v = n let i = 0 while (v >= 1024 && i < u.length - 1) { v /= 1024 i++ } return `${v.toFixed(1)} ${u[i]}` } export function DuplicatesPage() { const [libs, setLibs] = useState([]) const [libID, setLibID] = useState('') const [report, setReport] = useState(null) const [scanning, setScanning] = useState(false) useEffect(() => { libraryAPI.list().then(setLibs) }, []) useEffect(() => { duplicatesAPI.list(libID).then(setReport).catch(() => setReport(null)) }, [libID]) const scan = async () => { setScanning(true) try { const r = await duplicatesAPI.scan(libID) setReport(r) const cleaned = r.missing_removed ? `, 清理 ${r.missing_removed} 条失效记录` : '' toast.success(`扫描完成: ${r.groups_found} 组重复, ${r.items_marked} 项标记${cleaned}`) } catch (err: unknown) { const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error ?? '扫描失败' toast.error(msg) } finally { setScanning(false) } } const unmark = async () => { if (!(await confirmAction({ title: '清除重复标记', message: '清除所有重复标记?(磁盘文件不会被删除)', confirmText: '清除' }))) return const r = await duplicatesAPI.unmark(libID) toast.success(`已清除 ${r.unmarked} 项`) setReport(null) } return (

重复文件

通过稀疏采样 MD5(头部 / 中部 / 尾部各 1 MiB + 文件大小)检测重复媒体, 同一组中保留刮削过的较大文件作为主条目,其余标记为重复。

{report && report.groups_found === 0 && (

扫描了 {report.total_scanned} 项,未发现重复。

)} {report && report.missing_removed ? (

已清理 {report.missing_removed} 条文件不存在的媒体记录,统计容量会在刷新后恢复正常。

) : null} {report && (report.groups ?? []).map((g) => (

{g.hash}

主条目

{g.primary.title}

{g.primary.path} · {fmtBytes(g.primary.size_bytes)}

重复 ({g.duplicates.length})

{g.duplicates.map((d) => (
{d.title} · {d.path} · {fmtBytes(d.size_bytes)}
))}
))}
) }