refactor: 将文件移动改为独立接口,扫描完成后手动触发
This commit is contained in:
@@ -12,8 +12,6 @@ ffmpeg.path=
|
|||||||
organize.rotateImages=false
|
organize.rotateImages=false
|
||||||
# 探测/认类型/移动阶段的并发 worker 数(默认 4;0 表示用满全部 CPU 核数)
|
# 探测/认类型/移动阶段的并发 worker 数(默认 4;0 表示用满全部 CPU 核数)
|
||||||
organize.workers=8
|
organize.workers=8
|
||||||
# 扫描完成后是否移动文件到分类目录(true=移动,默认;false=只归类不移动,文件留在原位)
|
|
||||||
organize.moveFiles=false
|
|
||||||
# 色情图片识别(需本机安装 python 与 nudenet:pip install nudenet)
|
# 色情图片识别(需本机安装 python 与 nudenet:pip install nudenet)
|
||||||
# true=开启,判为色情的图片移入「图片_敏感_*」目录(明暗子目录照常);false=关闭(默认)
|
# true=开启,判为色情的图片移入「图片_敏感_*」目录(明暗子目录照常);false=关闭(默认)
|
||||||
# 模型:优先加载程序运行目录下 lib/nwfs/320n.onnx(推荐随程序分发,离线可用);
|
# 模型:优先加载程序运行目录下 lib/nwfs/320n.onnx(推荐随程序分发,离线可用);
|
||||||
|
|||||||
@@ -60,7 +60,12 @@
|
|||||||
<div v-else class="running-hint">
|
<div v-else class="running-hint">
|
||||||
<span>
|
<span>
|
||||||
<i class="el-icon-loading"></i>
|
<i class="el-icon-loading"></i>
|
||||||
扫描整理中,已运行 {{ status.elapsed }} 秒...({{ stageText }} {{ progressText }})
|
<template v-if="status.stage === 'move'">
|
||||||
|
移动文件中,已运行 {{ status.elapsed }} 秒...({{ progressText }})
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
扫描整理中,已运行 {{ status.elapsed }} 秒...({{ stageText }} {{ progressText }})
|
||||||
|
</template>
|
||||||
<span v-if="etaText" class="eta-text">预计剩余 {{ etaText }}</span>
|
<span v-if="etaText" class="eta-text">预计剩余 {{ etaText }}</span>
|
||||||
</span>
|
</span>
|
||||||
<el-progress
|
<el-progress
|
||||||
@@ -78,6 +83,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="status.state === 'done'" class="move-actions">
|
||||||
|
<el-button
|
||||||
|
type="warning"
|
||||||
|
size="medium"
|
||||||
|
icon="el-icon-s-promotion"
|
||||||
|
:loading="moving"
|
||||||
|
@click="handleMove">移动文件到分类目录</el-button>
|
||||||
|
<span class="move-hint">根据扫描归类结果,将文件移动到「存放目录」下的分类子目录</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="status.state === 'cancelled'"
|
v-if="status.state === 'cancelled'"
|
||||||
title="扫描已中止"
|
title="扫描已中止"
|
||||||
@@ -193,6 +208,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
// ---------- 开始扫描 ----------
|
// ---------- 开始扫描 ----------
|
||||||
starting: false, // 点完按钮后、等后端回话前的"转圈"
|
starting: false, // 点完按钮后、等后端回话前的"转圈"
|
||||||
|
moving: false, // 点完「移动文件」后等后端回话前的"转圈"
|
||||||
cancelling: false, // 点完「中止扫描」后等后端确认的"转圈"
|
cancelling: false, // 点完「中止扫描」后等后端确认的"转圈"
|
||||||
scanDir: localStorage.getItem('scan-dir') || '', // 要扫描的目录(记住上次填的)
|
scanDir: localStorage.getItem('scan-dir') || '', // 要扫描的目录(记住上次填的)
|
||||||
destDir: localStorage.getItem('scan-dest-dir') || '', // 整理后文件放哪(记住上次填的)
|
destDir: localStorage.getItem('scan-dest-dir') || '', // 整理后文件放哪(记住上次填的)
|
||||||
@@ -422,6 +438,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 触发文件移动:根据扫描归类的结果,将文件移动到分类目录
|
||||||
|
async handleMove() {
|
||||||
|
this.moving = true
|
||||||
|
try {
|
||||||
|
const res = await request.post('/api/scan/move')
|
||||||
|
this.applyStatus(res.data)
|
||||||
|
this.$message.success('已开始移动文件')
|
||||||
|
} finally {
|
||||||
|
this.moving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ==================== 目录选择弹窗 ====================
|
// ==================== 目录选择弹窗 ====================
|
||||||
|
|
||||||
// 打开弹窗;target 决定这次选的是"扫描目录"还是"存放目录"
|
// 打开弹窗;target 决定这次选的是"扫描目录"还是"存放目录"
|
||||||
@@ -607,6 +635,22 @@ export default {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.move-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fffbe6;
|
||||||
|
border: 1px solid #ffe58f;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-hint {
|
||||||
|
color: #8c6900;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.running-hint {
|
.running-hint {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -34,10 +34,11 @@ func NewRouter(st *store.Store) *gin.Engine {
|
|||||||
apiGroup.POST("/media/stats", mc.Stats)
|
apiGroup.POST("/media/stats", mc.Stats)
|
||||||
apiGroup.GET("/media/preview", mc.Preview)
|
apiGroup.GET("/media/preview", mc.Preview)
|
||||||
|
|
||||||
// 扫描整理(开始 / 中止 / 查进度 / 实时进度)
|
// 扫描整理(开始 / 中止 / 移动 / 查进度 / 实时进度)
|
||||||
sc := NewScanController(service.NewScanService(st))
|
sc := NewScanController(service.NewScanService(st))
|
||||||
apiGroup.POST("/scan/start", sc.Start)
|
apiGroup.POST("/scan/start", sc.Start)
|
||||||
apiGroup.POST("/scan/cancel", sc.Cancel)
|
apiGroup.POST("/scan/cancel", sc.Cancel)
|
||||||
|
apiGroup.POST("/scan/move", sc.Move)
|
||||||
apiGroup.GET("/scan/status", sc.Status)
|
apiGroup.GET("/scan/status", sc.Status)
|
||||||
apiGroup.GET("/scan/ws", sc.Ws)
|
apiGroup.GET("/scan/ws", sc.Ws)
|
||||||
|
|
||||||
|
|||||||
@@ -57,3 +57,14 @@ func (c *ScanController) Cancel(ctx *gin.Context) {
|
|||||||
}
|
}
|
||||||
Success(ctx, c.service.Status())
|
Success(ctx, c.service.Status())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move 处理 POST /api/scan/move:触发文件移动操作。
|
||||||
|
// 根据扫描时记录的分类结果,将文件从源路径移动到目标目录。
|
||||||
|
// 只有在扫描完成后才能调用。
|
||||||
|
func (c *ScanController) Move(ctx *gin.Context) {
|
||||||
|
if err := c.service.Move(); err != nil {
|
||||||
|
Error(ctx, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Success(ctx, c.service.Status())
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
// - FFmpegPath:ffmpeg 程序路径(用于把带 EXIF 旋转标记的图片转正);留空自动在 PATH 里找
|
// - FFmpegPath:ffmpeg 程序路径(用于把带 EXIF 旋转标记的图片转正);留空自动在 PATH 里找
|
||||||
// - RotateImages:是否处理图片 EXIF 旋转(默认 true);false 则完全不处理,按存储方向/尺寸归类
|
// - RotateImages:是否处理图片 EXIF 旋转(默认 true);false 则完全不处理,按存储方向/尺寸归类
|
||||||
// - Workers:探测/MIME/移动阶段的并发 worker 数(默认 4;设 0 表示用满全部 CPU 核数)
|
// - Workers:探测/MIME/移动阶段的并发 worker 数(默认 4;设 0 表示用满全部 CPU 核数)
|
||||||
// - MoveFiles:扫描完成后是否移动文件到分类目录(默认 true)
|
|
||||||
// - NSFWEnabled:是否开启色情图片识别(本地 python + NudeNet,默认 false)
|
// - NSFWEnabled:是否开启色情图片识别(本地 python + NudeNet,默认 false)
|
||||||
// - NSFWPython:python 可执行文件路径;留空自动在 PATH 里找
|
// - NSFWPython:python 可执行文件路径;留空自动在 PATH 里找
|
||||||
// - NSFWThreshold:判定阈值 0~1(默认 0.5)
|
// - NSFWThreshold:判定阈值 0~1(默认 0.5)
|
||||||
@@ -30,7 +29,6 @@ type Config struct {
|
|||||||
FFmpegPath string
|
FFmpegPath string
|
||||||
RotateImages bool
|
RotateImages bool
|
||||||
Workers int
|
Workers int
|
||||||
MoveFiles bool
|
|
||||||
NSFWEnabled bool
|
NSFWEnabled bool
|
||||||
NSFWPython string
|
NSFWPython string
|
||||||
NSFWThreshold float64
|
NSFWThreshold float64
|
||||||
@@ -47,7 +45,7 @@ func GetAppConfig() Config {
|
|||||||
// LoadAppConfig 从 application.properties 读配置;文件不存在就用默认值。
|
// LoadAppConfig 从 application.properties 读配置;文件不存在就用默认值。
|
||||||
func LoadAppConfig() {
|
func LoadAppConfig() {
|
||||||
// 先给一套默认值
|
// 先给一套默认值
|
||||||
appConfig = Config{Port: 8080, Dsn: "media-info.db", AutoOpen: true, RotateImages: true, Workers: 4, MoveFiles: true, NSFWThreshold: 0.5}
|
appConfig = Config{Port: 8080, Dsn: "media-info.db", AutoOpen: true, RotateImages: true, Workers: 4, NSFWThreshold: 0.5}
|
||||||
|
|
||||||
p, err := properties.LoadFile("application.properties", properties.UTF8)
|
p, err := properties.LoadFile("application.properties", properties.UTF8)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -72,8 +70,6 @@ func LoadAppConfig() {
|
|||||||
appConfig.Workers = n
|
appConfig.Workers = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 扫描完成后是否移动文件:默认 true
|
|
||||||
appConfig.MoveFiles = p.GetBool("organize.moveFiles", true)
|
|
||||||
// 色情识别:默认关;python 路径留空自动在 PATH 找;阈值只认 0~1 之间的小数
|
// 色情识别:默认关;python 路径留空自动在 PATH 找;阈值只认 0~1 之间的小数
|
||||||
appConfig.NSFWEnabled = p.GetBool("nsfw.enabled", false)
|
appConfig.NSFWEnabled = p.GetBool("nsfw.enabled", false)
|
||||||
if v, ok := p.Get("nsfw.python"); ok && v != "" {
|
if v, ok := p.Get("nsfw.python"); ok && v != "" {
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ type Organizer struct {
|
|||||||
ctx context.Context // 中止信号;ctx 被取消时探测/归类立即停下
|
ctx context.Context // 中止信号;ctx 被取消时探测/归类立即停下
|
||||||
nsfw *nsfw.Detector // 色情检测器;为 nil 表示没启用,图片不检测
|
nsfw *nsfw.Detector // 色情检测器;为 nil 表示没启用,图片不检测
|
||||||
rotateImages bool // 是否处理 EXIF 旋转(转正/按显示方向归类);false 则完全不动,按存储方向归类
|
rotateImages bool // 是否处理 EXIF 旋转(转正/按显示方向归类);false 则完全不动,按存储方向归类
|
||||||
moveFiles bool // 扫描完成后是否移动文件;false 时禁用续传跳过,每次都完整探测
|
|
||||||
buckets map[string][]string // 目标目录 → 里面该放哪些文件(只记源路径)
|
buckets map[string][]string // 目标目录 → 里面该放哪些文件(只记源路径)
|
||||||
order []string // 目标目录的先后顺序(保证输出稳定、不来回跳)
|
order []string // 目标目录的先后顺序(保证输出稳定、不来回跳)
|
||||||
onProgress func(phase string, done, total int)
|
onProgress func(phase string, done, total int)
|
||||||
@@ -136,13 +135,6 @@ func (o *Organizer) SetRotateImages(b bool) {
|
|||||||
o.rotateImages = b
|
o.rotateImages = b
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMoveFiles 设置扫描完成后是否移动文件(默认 true)。
|
|
||||||
// - true:文件会被移动到分类目录,启用续传优化
|
|
||||||
// - false:文件留在原位,禁用续传跳过,每次都完整探测并记录到媒体库
|
|
||||||
func (o *Organizer) SetMoveFiles(b bool) {
|
|
||||||
o.moveFiles = b
|
|
||||||
}
|
|
||||||
|
|
||||||
// report 上报进度。total 为 0 表示这一步没活干,就不用报了。
|
// report 上报进度。total 为 0 表示这一步没活干,就不用报了。
|
||||||
func (o *Organizer) report(phase string, done, total int) {
|
func (o *Organizer) report(phase string, done, total int) {
|
||||||
if o.onProgress != nil && total > 0 {
|
if o.onProgress != nil && total > 0 {
|
||||||
@@ -218,12 +210,8 @@ func (o *Organizer) addToDir(f scanner.File, dir string) {
|
|||||||
|
|
||||||
// addClassified 归档一个文件并记进续传表:把"源路径 → 目标目录"写进 scan_log,
|
// addClassified 归档一个文件并记进续传表:把"源路径 → 目标目录"写进 scan_log,
|
||||||
// 这样下次重扫时这个文件直接复用目标目录,不再重新探测/识别。
|
// 这样下次重扫时这个文件直接复用目标目录,不再重新探测/识别。
|
||||||
// 注意:moveFiles=false 时不写续传记录,因为文件没有真正移动。
|
|
||||||
func (o *Organizer) addClassified(f scanner.File, subDir string, size int64, headMd5, tailMd5 string) {
|
func (o *Organizer) addClassified(f scanner.File, subDir string, size int64, headMd5, tailMd5 string) {
|
||||||
o.add(f, subDir)
|
o.add(f, subDir)
|
||||||
if !o.moveFiles {
|
|
||||||
return // 不移动文件时不写续传记录
|
|
||||||
}
|
|
||||||
if o.store == nil {
|
if o.store == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -241,11 +229,7 @@ func (o *Organizer) addClassified(f scanner.File, subDir string, size int64, hea
|
|||||||
|
|
||||||
// matchResume 判断一个文件上次是否已归类过,且内容没变。
|
// matchResume 判断一个文件上次是否已归类过,且内容没变。
|
||||||
// 命中返回上次的目标目录和 ok=true;内容变了/没记录则 ok=false(要重新探测)。
|
// 命中返回上次的目标目录和 ok=true;内容变了/没记录则 ok=false(要重新探测)。
|
||||||
// 注意:moveFiles=false 时禁用续传跳过,每次都完整探测。
|
|
||||||
func (o *Organizer) matchResume(f scanner.File) (store.ScanLog, bool) {
|
func (o *Organizer) matchResume(f scanner.File) (store.ScanLog, bool) {
|
||||||
if !o.moveFiles {
|
|
||||||
return store.ScanLog{}, false // 不移动文件时禁用续传
|
|
||||||
}
|
|
||||||
if o.resume == nil {
|
if o.resume == nil {
|
||||||
return store.ScanLog{}, false
|
return store.ScanLog{}, false
|
||||||
}
|
}
|
||||||
|
|||||||
+134
-10
@@ -24,6 +24,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"kenaito-media-picker/internal/config"
|
"kenaito-media-picker/internal/config"
|
||||||
|
"kenaito-media-picker/internal/mover"
|
||||||
"kenaito-media-picker/internal/nsfw"
|
"kenaito-media-picker/internal/nsfw"
|
||||||
"kenaito-media-picker/internal/organize"
|
"kenaito-media-picker/internal/organize"
|
||||||
"kenaito-media-picker/internal/probe"
|
"kenaito-media-picker/internal/probe"
|
||||||
@@ -234,6 +235,135 @@ func (s *ScanService) Cancel() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Move 触发文件移动操作。
|
||||||
|
// 根据 scan_log 中记录的分类结果,将文件从源路径移动到目标目录。
|
||||||
|
// 只有在扫描完成(state == done)且当前没有任务运行时才能调用。
|
||||||
|
func (s *ScanService) Move() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
|
||||||
|
// 正在跑就不能再开一个,直接拒绝
|
||||||
|
if s.state == ScanRunning {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return errors.New("扫描整理正在运行中,请等待完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必须先完成一次扫描,才有 scan_log 记录
|
||||||
|
if s.state != ScanDone {
|
||||||
|
s.mu.Unlock()
|
||||||
|
return errors.New("请先完成扫描后再执行移动")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 把状态改成"运行中",清空上一次的残留信息
|
||||||
|
s.state = ScanRunning
|
||||||
|
s.msg = ""
|
||||||
|
s.started = time.Now()
|
||||||
|
s.lines = nil
|
||||||
|
s.stage = stageMove
|
||||||
|
s.done = 0
|
||||||
|
s.total = 0
|
||||||
|
s.ctx, s.cancel = context.WithCancel(context.Background())
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
go s.runMove(s.ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runMove 是移动文件的后台任务。
|
||||||
|
// 它从数据库读取 scan_log 记录,构建移动任务列表,然后并行执行文件移动。
|
||||||
|
func (s *ScanService) runMove(ctx context.Context) {
|
||||||
|
// 创建日志管道(和 run() 一样,截取 fmt.Println 输出)
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
s.finish(ScanError, "创建日志管道失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
sc := bufio.NewScanner(r)
|
||||||
|
sc.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||||
|
for sc.Scan() {
|
||||||
|
line := sanitizeLine(sc.Text())
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
if len(s.lines) >= maxLogLines {
|
||||||
|
s.lines = append([]string{}, s.lines[len(s.lines)/2:]...)
|
||||||
|
}
|
||||||
|
s.lines = append(s.lines, line)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
_ = w.Close()
|
||||||
|
<-done
|
||||||
|
if rcv := recover(); rcv != nil {
|
||||||
|
s.finish(ScanError, fmt.Sprintf("移动文件内部错误: %v\n%s", rcv, debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 从数据库读取 scan_log 记录
|
||||||
|
if s.store == nil {
|
||||||
|
s.finish(ScanError, "数据库不可用,无法执行移动")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logs, err := s.store.LoadScanLog()
|
||||||
|
if err != nil {
|
||||||
|
s.finish(ScanError, "加载续传记录失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(logs) == 0 {
|
||||||
|
fmt.Println("=== 没有待移动的文件(scan_log 为空)")
|
||||||
|
s.finish(ScanDone, "没有待移动的文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建移动任务列表
|
||||||
|
jobs := make([]mover.MoveJob, 0, len(logs))
|
||||||
|
for _, sl := range logs {
|
||||||
|
// 检查源文件是否还存在
|
||||||
|
if _, err := os.Stat(sl.SourcePath); err != nil {
|
||||||
|
fmt.Printf("=== 源文件不存在,跳过: %s\n", sl.SourcePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
jobs = append(jobs, mover.MoveJob{Source: sl.SourcePath, TargetDir: sl.TargetDir})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(jobs) == 0 {
|
||||||
|
fmt.Println("=== 所有源文件均已不存在,无需移动")
|
||||||
|
s.finish(ScanDone, "没有需要移动的文件(源文件已不存在)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("=== 开始移动文件,共 %d 个任务\n", len(jobs))
|
||||||
|
s.setProgress(stageMove, 0, len(jobs))
|
||||||
|
|
||||||
|
// 执行移动
|
||||||
|
mv := &mover.Mover{}
|
||||||
|
mv.MoveAll(jobs, func(doneCount int) {
|
||||||
|
s.setProgress(stageMove, doneCount, len(jobs))
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
s.finish(ScanCancelled, "移动已中止")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("=== 文件移动完成")
|
||||||
|
s.finish(ScanDone, fmt.Sprintf("文件移动完成,共移动 %d 个文件", len(jobs)))
|
||||||
|
}
|
||||||
|
|
||||||
// Status 生成一份"当前进度快照"返回给前端。
|
// Status 生成一份"当前进度快照"返回给前端。
|
||||||
func (s *ScanService) Status() ScanStatus {
|
func (s *ScanService) Status() ScanStatus {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@@ -530,7 +660,6 @@ func (s *ScanService) runScan(ctx context.Context, rootDir, destDir string) erro
|
|||||||
org := organize.New(pickerDir, ffprobePath, ffmpegPath, s.store, ctx)
|
org := organize.New(pickerDir, ffprobePath, ffmpegPath, s.store, ctx)
|
||||||
org.SetNSFWDetector(nsfwDet)
|
org.SetNSFWDetector(nsfwDet)
|
||||||
org.SetRotateImages(config.GetAppConfig().RotateImages)
|
org.SetRotateImages(config.GetAppConfig().RotateImages)
|
||||||
org.SetMoveFiles(config.GetAppConfig().MoveFiles)
|
|
||||||
|
|
||||||
// 整个"整理"分两个阶段,界面上共用一条进度条:
|
// 整个"整理"分两个阶段,界面上共用一条进度条:
|
||||||
// 归类阶段:图片、视频/音频分两批处理。进度 = 已处理文件数 / 全部待整理文件数
|
// 归类阶段:图片、视频/音频分两批处理。进度 = 已处理文件数 / 全部待整理文件数
|
||||||
@@ -566,17 +695,12 @@ func (s *ScanService) runScan(ctx context.Context, rootDir, destDir string) erro
|
|||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动:把文件搬进各自的分类目录,并记录去向
|
// 扫描完成后只写入媒体记录和续传记录,不移动文件。
|
||||||
if config.GetAppConfig().MoveFiles {
|
// 文件移动由用户在页面上手动触发(POST /api/scan/move)。
|
||||||
org.Flush()
|
org.FlushRecordsOnly()
|
||||||
} else {
|
|
||||||
// 不移动文件,但仍需写入媒体记录到数据库
|
|
||||||
org.FlushRecordsOnly()
|
|
||||||
fmt.Println("=== 配置 organize.moveFiles=false,跳过文件移动(媒体记录已保存)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 把进度条顶满 100%,再打印"整理完成"
|
// 把进度条顶满 100%,再打印"整理完成"
|
||||||
s.setProgress(stageMove, totalWork, totalWork)
|
s.setProgress(stageMove, totalWork, totalWork)
|
||||||
fmt.Println("=== 整理完成")
|
fmt.Println("=== 扫描整理完成,请在页面点击「移动文件」按钮执行文件移动")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user