feat: 扫描整理支持中止;存放目录始终创建 KMP已整理 层

This commit is contained in:
2025-05-31 21:09:26 +08:00
parent 0aa9d44025
commit ea88f3e766
6 changed files with 157 additions and 32 deletions
+24 -4
View File
@@ -11,6 +11,7 @@
package organize
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -59,6 +60,7 @@ type Organizer struct {
ffmpegPath string // ffmpeg 程序路径;为空表示没有转正 EXIF 旋转图片的能力
store *store.Store // 数据库;为 nil 表示不记录媒体信息
mover *mover.Mover // 负责实际搬文件的家伙
ctx context.Context // 中止信号;ctx 被取消时探测/归类立即停下
buckets map[string][]string // 目标目录 → 里面该放哪些文件(只记源路径)
order []string // 目标目录的先后顺序(保证输出稳定、不来回跳)
onProgress func(phase string, done, total int)
@@ -66,13 +68,15 @@ type Organizer struct {
}
// New 创建整理器。store 可以为 nil——数据库打不开时,整理照常进行,只是不记入库信息。
func New(pickerDir, ffprobePath, ffmpegPath string, st *store.Store) *Organizer {
// ctx 可以为 nil——不提供中止能力,任务跑到完为止。
func New(pickerDir, ffprobePath, ffmpegPath string, st *store.Store, ctx context.Context) *Organizer {
return &Organizer{
pickerDir: pickerDir,
ffprobePath: ffprobePath,
ffmpegPath: ffmpegPath,
store: st,
mover: &mover.Mover{},
ctx: ctx,
buckets: make(map[string][]string),
}
}
@@ -104,7 +108,9 @@ func workerCount() int {
// 通俗理解:一条流水线有 N 个工人,每个工人从传送带上拿一个活干完,
// 干完把结果按编号放回架子上(results[i] 对应 items[i] 的结果)。
// onProgress 每干完一个活就报一次数(已完成几个),线程安全、只会涨不会退。
func runParallel[T any, R any](items []T, fn func(T) R, onProgress func(done int)) []R {
// ctx 被取消时工人不再干活(任务照常消费,避免发送方堵死),
// 被跳过的位置留零值,调用方看到 ctx.Err() 会自行停下。
func runParallel[T any, R any](ctx context.Context, items []T, fn func(T) R, onProgress func(done int)) []R {
results := make([]R, len(items))
if len(items) == 0 {
return results
@@ -121,6 +127,10 @@ func runParallel[T any, R any](items []T, fn func(T) R, onProgress func(done int
go func() {
defer wg.Done()
for i := range jobs {
// 用户点了中止:不再干活,但任务还要继续消费,免得主 goroutine 堵死在发送上
if ctx != nil && ctx.Err() != nil {
continue
}
results[i] = fn(items[i])
if onProgress != nil {
onProgress(int(atomic.AddInt32(&doneCount, 1)))
@@ -350,10 +360,15 @@ func (o *Organizer) ProcessImages(images []scanner.File) {
// 统计结果,最后打一行总结给用户看
success, fail, ignore, dup := 0, 0, 0, 0
total := len(images)
results := runParallel(images, o.probeImage, func(done int) {
results := runParallel(o.ctx, images, o.probeImage, func(done int) {
o.report(PhaseImages, done, total)
})
for i, r := range results {
// 用户点了中止:停下归类,已分堆的交给 runScan 决定要不要搬
if o.ctx != nil && o.ctx.Err() != nil {
fmt.Println("=== 扫描已中止,跳过剩余图片归类")
break
}
f := images[i]
switch {
case r.psd:
@@ -418,10 +433,15 @@ func (o *Organizer) probeMedia(f scanner.File) mediaProbeResult {
func (o *Organizer) ProcessMedia(media []scanner.File) {
video, audio, fail, ignore, dup := 0, 0, 0, 0, 0
total := len(media)
results := runParallel(media, o.probeMedia, func(done int) {
results := runParallel(o.ctx, media, o.probeMedia, func(done int) {
o.report(PhaseMedia, done, total)
})
for i, r := range results {
// 用户点了中止:停下归类,已分堆的交给 runScan 决定要不要搬
if o.ctx != nil && o.ctx.Err() != nil {
fmt.Println("=== 扫描已中止,跳过剩余媒体归类")
break
}
f := media[i]
// ---------- 体检失败 ----------
if r.err != nil {