feat: 扫描整理并行化、WebSocket 实时进度与统计看板

- 移动阶段并行处理文件,按原子计数上报累计进度
- 扫描遍历阶段实时上报已发现文件数,界面不再卡在 0/0
- 新增 /api/scan/ws WebSocket 实时推送扫描状态;前端断线指数退避自动重连,并以低频轮询兜底
- 新增统计看板与媒体库筛选/排序,目录选择器路径不存在时回退到最近可用父目录
- 存放目录支持自定义并持久化,默认输出到 KMP已整理
This commit is contained in:
2025-05-08 21:16:20 +08:00
parent 36d7ecced8
commit f915a9b1ac
22 changed files with 1342 additions and 281 deletions
+27 -18
View File
@@ -6,11 +6,11 @@ import (
"os"
"path/filepath"
"strings"
"time"
"kenaito-media-picker/internal/probe"
"github.com/gabriel-vasile/mimetype"
"github.com/redmask-hb/GoSimplePrint/goPrint"
)
// File 扫描到的文件信息
@@ -30,14 +30,20 @@ type Result struct {
// Options 扫描选项
type Options struct {
RootDir string // 扫描根目录
SkipDir string // 白名单输出目录,整目录跳过
SkipNames []string // 按文件名跳过(数据库、ffprobe 等)
RootDir string // 扫描根目录
SkipDir string // 白名单输出目录,整目录跳过
SkipDirNames []string // 按目录名跳过(如 KMP已整理),任意位置命中即跳过
SkipNames []string // 按文件名跳过(数据库、ffprobe 等)
OnProgress func(done, total int)
}
// Scan 递归扫描目录并读取每个文件的 MIME 信息
func Scan(opts Options) (*Result, error) {
res := &Result{}
// 遍历阶段尚不知道总文件数,先按「已发现文件数」实时上报(total=0),
// 并节流上报频率,避免目录很大时界面长时间无动静或推送过密。
walkCount := 0
lastReport := time.Now()
err := filepath.Walk(opts.RootDir, func(currentPath string, info os.FileInfo, err error) error {
if err != nil {
// 单个路径访问失败(权限不足/损坏)不中止整体扫描,仅告警后跳过该子树
@@ -48,6 +54,11 @@ func Scan(opts Options) (*Result, error) {
if opts.SkipDir != "" && currentPath == opts.SkipDir {
return filepath.SkipDir
}
for _, n := range opts.SkipDirNames {
if info.Name() == n {
return filepath.SkipDir
}
}
return nil
}
for _, n := range opts.SkipNames {
@@ -64,6 +75,11 @@ func Scan(opts Options) (*Result, error) {
Name: info.Name(),
Ext: filepath.Ext(currentPath),
})
walkCount++
if opts.OnProgress != nil && time.Since(lastReport) >= 100*time.Millisecond {
lastReport = time.Now()
opts.OnProgress(walkCount, 0)
}
return nil
})
if err != nil {
@@ -72,15 +88,16 @@ func Scan(opts Options) (*Result, error) {
total := len(res.Files)
fmt.Printf("=== 文件总数: %d \n", total)
bar := goPrint.NewBar(100)
bar.SetNotice("=== 扫描文件Mime:")
bar.SetGraph(">")
bar.SetNoticeColor(goPrint.FontColor.Red)
// 遍历结束、开始 MIME 探测前,上报一次完整总数
if opts.OnProgress != nil {
opts.OnProgress(total, total)
}
for i := range res.Files {
res.Files[i].Mime = detectMime(res.Files[i].Path)
bar.PrintBar(percent(i+1, total))
if opts.OnProgress != nil {
opts.OnProgress(i+1, total)
}
}
bar.PrintEnd("=== Finish")
return res, nil
}
@@ -138,11 +155,3 @@ func isPotentialMedia(fileMime string) bool {
}
return true
}
// percent 计算百分比(0~100),total 为 0 时返回 100
func percent(part, total int) int {
if total <= 0 {
return 100
}
return int(float64(part) / float64(total) * 100)
}