refactor: 合并 NSFW 数据到 media_info 表
This commit is contained in:
@@ -72,11 +72,11 @@ type Organizer struct {
|
||||
ctx context.Context // 中止信号;ctx 被取消时探测/归类立即停下
|
||||
nsfw *nsfw.Detector // 色情检测器;为 nil 表示没启用,图片不检测
|
||||
rotateImages bool // 是否处理 EXIF 旋转(转正/按显示方向归类);false 则完全不动,按存储方向归类
|
||||
moveFiles bool // 扫描完成后是否移动文件;false 时禁用续传跳过,每次都完整探测
|
||||
buckets map[string][]string // 目标目录 → 里面该放哪些文件(只记源路径)
|
||||
order []string // 目标目录的先后顺序(保证输出稳定、不来回跳)
|
||||
onProgress func(phase string, done, total int)
|
||||
pending []*store.MediaInfo // 待写入数据库的媒体记录(攒一批再批量写)
|
||||
pendingNsfw []store.NsfwRecord // 待写入数据库的色情识别记录(攒一批再批量写)
|
||||
resume map[string]store.ScanLog // 续传表:源路径 → 上次归类结果(重扫跳过重新探测)
|
||||
resumeSkip int32 // 本次续传跳过的文件数(并行累计)
|
||||
pendingScan []store.ScanLog // 待写入续传表的记录(攒一批再批量写)
|
||||
@@ -136,6 +136,13 @@ func (o *Organizer) SetRotateImages(b bool) {
|
||||
o.rotateImages = b
|
||||
}
|
||||
|
||||
// SetMoveFiles 设置扫描完成后是否移动文件(默认 true)。
|
||||
// - true:文件会被移动到分类目录,启用续传优化
|
||||
// - false:文件留在原位,禁用续传跳过,每次都完整探测并记录到媒体库
|
||||
func (o *Organizer) SetMoveFiles(b bool) {
|
||||
o.moveFiles = b
|
||||
}
|
||||
|
||||
// report 上报进度。total 为 0 表示这一步没活干,就不用报了。
|
||||
func (o *Organizer) report(phase string, done, total int) {
|
||||
if o.onProgress != nil && total > 0 {
|
||||
@@ -211,8 +218,12 @@ func (o *Organizer) addToDir(f scanner.File, dir string) {
|
||||
|
||||
// addClassified 归档一个文件并记进续传表:把"源路径 → 目标目录"写进 scan_log,
|
||||
// 这样下次重扫时这个文件直接复用目标目录,不再重新探测/识别。
|
||||
// 注意:moveFiles=false 时不写续传记录,因为文件没有真正移动。
|
||||
func (o *Organizer) addClassified(f scanner.File, subDir string, size int64, headMd5, tailMd5 string) {
|
||||
o.add(f, subDir)
|
||||
if !o.moveFiles {
|
||||
return // 不移动文件时不写续传记录
|
||||
}
|
||||
if o.store == nil {
|
||||
return
|
||||
}
|
||||
@@ -230,7 +241,11 @@ func (o *Organizer) addClassified(f scanner.File, subDir string, size int64, hea
|
||||
|
||||
// matchResume 判断一个文件上次是否已归类过,且内容没变。
|
||||
// 命中返回上次的目标目录和 ok=true;内容变了/没记录则 ok=false(要重新探测)。
|
||||
// 注意:moveFiles=false 时禁用续传跳过,每次都完整探测。
|
||||
func (o *Organizer) matchResume(f scanner.File) (store.ScanLog, bool) {
|
||||
if !o.moveFiles {
|
||||
return store.ScanLog{}, false // 不移动文件时禁用续传
|
||||
}
|
||||
if o.resume == nil {
|
||||
return store.ScanLog{}, false
|
||||
}
|
||||
@@ -321,11 +336,14 @@ func (o *Organizer) isDuplicate(md5Str, path string) bool {
|
||||
}
|
||||
|
||||
// record 把一份媒体信息记到"待入库"缓冲里(先攒着,最后统一批量写库)。
|
||||
func (o *Organizer) record(f scanner.File, mediaType string, res *probe.ProbeResult, md5Str, headMd5, tailMd5 string, size int64) {
|
||||
// 返回创建的 MediaInfo 指针,调用方可继续设置额外字段(如 NsfwScore)。
|
||||
func (o *Organizer) record(f scanner.File, mediaType string, res *probe.ProbeResult, md5Str, headMd5, tailMd5 string, size int64) *store.MediaInfo {
|
||||
if o.store == nil || md5Str == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
o.pending = append(o.pending, buildMediaInfo(f, mediaType, res, md5Str, headMd5, tailMd5, size))
|
||||
info := buildMediaInfo(f, mediaType, res, md5Str, headMd5, tailMd5, size)
|
||||
o.pending = append(o.pending, info)
|
||||
return info
|
||||
}
|
||||
|
||||
// buildMediaInfo 把探测结果整理成一条要入库的记录。
|
||||
@@ -553,16 +571,14 @@ func (o *Organizer) ProcessImages(images []scanner.File) {
|
||||
default:
|
||||
// 正常图片:按分辨率/方向/明暗归入对应的分类目录;
|
||||
// 被判为色情的改去「图片_敏感_*」目录,明暗子目录照常保留
|
||||
o.record(f, "image", r.res, r.md5Str, r.headMd5, r.tailMd5, r.size)
|
||||
info := o.record(f, "image", r.res, r.md5Str, r.headMd5, r.tailMd5, r.size)
|
||||
if r.nsfw {
|
||||
o.addClassified(f, classify.ImageDirSensitive(f.Ext, r.res.Width, r.res.Height, r.darkScore, r.lightScore), r.size, r.headMd5, r.tailMd5)
|
||||
// 攒一条色情识别记录,最后连同"搬去哪了"一起写进数据库
|
||||
o.pendingNsfw = append(o.pendingNsfw, store.NsfwRecord{
|
||||
FilePath: f.Path,
|
||||
FileName: filepath.Base(f.Path),
|
||||
Score: r.nsfwScore,
|
||||
Threshold: o.nsfw.Threshold(),
|
||||
})
|
||||
// NSFW 结果直接写入 MediaInfo 记录
|
||||
if info != nil {
|
||||
info.NsfwScore = r.nsfwScore
|
||||
info.NsfwThreshold = o.nsfw.Threshold()
|
||||
}
|
||||
nsfwCount++
|
||||
} else {
|
||||
o.addClassified(f, classify.ImageDir(f.Ext, r.res.Width, r.res.Height, r.darkScore, r.lightScore), r.size, r.headMd5, r.tailMd5)
|
||||
@@ -716,13 +732,19 @@ func (o *Organizer) ProcessMedia(media []scanner.File) {
|
||||
|
||||
// ============================ 移动(最后一步) ============================
|
||||
|
||||
// FlushRecordsOnly 只写入数据库记录,不移动文件。
|
||||
// 用于 moveFiles=false 时,仍然保存媒体信息和续传记录。
|
||||
func (o *Organizer) FlushRecordsOnly() {
|
||||
o.flushRecords()
|
||||
o.flushScanLog()
|
||||
}
|
||||
|
||||
// Flush 收尾两步:
|
||||
// 1. 把之前攒的媒体记录一次性写进数据库;
|
||||
// 2. 把每个分类目录攒的文件**全局并行**搬过去(所有目录的文件一起搬)。
|
||||
// 不需要记录每个文件搬去哪了——文件就在分类目录里,一眼能看到。
|
||||
func (o *Organizer) Flush() {
|
||||
o.flushRecords()
|
||||
o.flushScanLog()
|
||||
o.FlushRecordsOnly()
|
||||
|
||||
// 把 buckets 里所有文件拼成一张"搬运清单"
|
||||
jobs := make([]mover.MoveJob, 0)
|
||||
@@ -734,27 +756,7 @@ func (o *Organizer) Flush() {
|
||||
|
||||
// 全局并行搬完:不管文件分散在几个目录,所有文件同时开搬。
|
||||
// onProgress 收到的是全局累计已搬数,正好和总数对得上。
|
||||
// moved 是"源路径 → 新位置"的对照表,色情记录要用它补上整理后的实际路径。
|
||||
moved := o.mover.MoveAll(jobs, func(done int) {
|
||||
o.mover.MoveAll(jobs, func(done int) {
|
||||
o.report(PhaseMove, done, len(jobs))
|
||||
})
|
||||
|
||||
// 把色情识别记录补上"整理后位置"(预览就用它),再批量写进数据库
|
||||
if len(o.pendingNsfw) > 0 {
|
||||
records := make([]*store.NsfwRecord, 0, len(o.pendingNsfw))
|
||||
for i := range o.pendingNsfw {
|
||||
if target, ok := moved[o.pendingNsfw[i].FilePath]; ok {
|
||||
o.pendingNsfw[i].MovedPath = target
|
||||
} else {
|
||||
o.pendingNsfw[i].MovedPath = o.pendingNsfw[i].FilePath // 没搬走就用源路径
|
||||
}
|
||||
records = append(records, &o.pendingNsfw[i])
|
||||
}
|
||||
if o.store != nil {
|
||||
if err := o.store.SaveNsfwBatch(records); err != nil {
|
||||
fmt.Printf("=== 保存色情识别记录失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
o.pendingNsfw = nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user