refactor: 合并 NSFW 数据到 media_info 表
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSaveNsfwBatchLarge(t *testing.T) {
|
||||
db := filepath.Join(t.TempDir(), "test.db")
|
||||
s, err := Open(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
s.Close()
|
||||
os.Remove(db)
|
||||
}()
|
||||
|
||||
// 3000 条,远超 SQLite 999 变量上限(旧代码会报 too many SQL variables)
|
||||
records := make([]*NsfwRecord, 0, 3000)
|
||||
for i := 0; i < 3000; i++ {
|
||||
records = append(records, &NsfwRecord{
|
||||
FilePath: fmt.Sprintf("/path/%d.jpg", i),
|
||||
FileName: fmt.Sprintf("%d.jpg", i),
|
||||
Score: 0.5,
|
||||
Threshold: 0.2,
|
||||
MovedPath: fmt.Sprintf("/moved/%d.jpg", i),
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
if err := s.SaveNsfwBatch(records); err != nil {
|
||||
t.Fatalf("SaveNsfwBatch: %v", err)
|
||||
}
|
||||
list, err := s.ListNsfw()
|
||||
if err != nil {
|
||||
t.Fatalf("ListNsfw: %v", err)
|
||||
}
|
||||
if len(list) != 3000 {
|
||||
t.Fatalf("got %d records, want 3000", len(list))
|
||||
}
|
||||
}
|
||||
+27
-80
@@ -31,24 +31,26 @@ func init() {
|
||||
|
||||
// MediaInfo 一条媒体文件的记录:文件本身的信息——路径、名字、大小、类型、宽高、编码、格式等。
|
||||
type MediaInfo struct {
|
||||
Id int64 `xorm:"pk autoincr 'id'" json:"id"`
|
||||
FileMd5 string `xorm:"char(32) index 'file_md5'" json:"fileMd5"` // 文件 MD5,内容相同则相同,用来判断是不是同一个文件
|
||||
FilePath string `xorm:"text 'file_path'" json:"filePath"` // 扫描时的源路径(在哪发现的)
|
||||
FileName string `xorm:"varchar(255) 'file_name'" json:"fileName"` // 文件名(含扩展名)
|
||||
FileExt string `xorm:"varchar(32) 'file_ext'" json:"fileExt"` // 扩展名(如 .jpg)
|
||||
FileSize int64 `xorm:"bigint 'file_size'" json:"fileSize"` // 文件大小(字节)
|
||||
MimeType string `xorm:"varchar(128) 'mime_type'" json:"mimeType"` // MIME 类型(如 image/jpeg)
|
||||
MediaType string `xorm:"varchar(16) index 'media_type'" json:"mediaType"` // 大类型:image / video / audio
|
||||
Width int `xorm:"int index 'width'" json:"width"` // 宽(像素)
|
||||
Height int `xorm:"int index 'height'" json:"height"` // 高(像素)
|
||||
Duration float64 `xorm:"real 'duration'" json:"duration"` // 时长(秒)
|
||||
Codec string `xorm:"varchar(64) 'codec'" json:"codec"` // 编码,如 h264 / hevc
|
||||
Format string `xorm:"varchar(64) 'format'" json:"format"` // 容器格式,如 mp4 / mkv
|
||||
RawInfo string `xorm:"text 'raw_info'" json:"rawInfo"` // ffprobe 读出的原始信息(JSON),留底备用
|
||||
HeadMd5 string `xorm:"char(32) index('idx_fp') 'head_md5'" json:"headMd5"` // 文件头 256KB 的 MD5(指纹用)
|
||||
TailMd5 string `xorm:"char(32) index('idx_fp') 'tail_md5'" json:"tailMd5"` // 文件尾 256KB 的 MD5(指纹用)
|
||||
CreatedAt time.Time `xorm:"created 'created_at'" json:"createdAt"` // 首次入库时间
|
||||
UpdatedAt time.Time `xorm:"updated 'updated_at'" json:"updatedAt"` // 最近更新时间
|
||||
Id int64 `xorm:"pk autoincr 'id'" json:"id"`
|
||||
FileMd5 string `xorm:"char(32) index 'file_md5'" json:"fileMd5"` // 文件 MD5,内容相同则相同,用来判断是不是同一个文件
|
||||
FilePath string `xorm:"text 'file_path'" json:"filePath"` // 扫描时的源路径(在哪发现的)
|
||||
FileName string `xorm:"varchar(255) 'file_name'" json:"fileName"` // 文件名(含扩展名)
|
||||
FileExt string `xorm:"varchar(32) 'file_ext'" json:"fileExt"` // 扩展名(如 .jpg)
|
||||
FileSize int64 `xorm:"bigint 'file_size'" json:"fileSize"` // 文件大小(字节)
|
||||
MimeType string `xorm:"varchar(128) 'mime_type'" json:"mimeType"` // MIME 类型(如 image/jpeg)
|
||||
MediaType string `xorm:"varchar(16) index 'media_type'" json:"mediaType"` // 大类型:image / video / audio
|
||||
Width int `xorm:"int index 'width'" json:"width"` // 宽(像素)
|
||||
Height int `xorm:"int index 'height'" json:"height"` // 高(像素)
|
||||
Duration float64 `xorm:"real 'duration'" json:"duration"` // 时长(秒)
|
||||
Codec string `xorm:"varchar(64) 'codec'" json:"codec"` // 编码,如 h264 / hevc
|
||||
Format string `xorm:"varchar(64) 'format'" json:"format"` // 容器格式,如 mp4 / mkv
|
||||
RawInfo string `xorm:"text 'raw_info'" json:"rawInfo"` // ffprobe 读出的原始信息(JSON),留底备用
|
||||
HeadMd5 string `xorm:"char(32) index('idx_fp') 'head_md5'" json:"headMd5"` // 文件头 256KB 的 MD5(指纹用)
|
||||
TailMd5 string `xorm:"char(32) index('idx_fp') 'tail_md5'" json:"tailMd5"` // 文件尾 256KB 的 MD5(指纹用)
|
||||
NsfwScore float64 `xorm:"real 'nsfw_score'" json:"nsfwScore"` // NSFW 检测分 0~1(0 表示未检测或非图片)
|
||||
NsfwThreshold float64 `xorm:"real 'nsfw_threshold'" json:"nsfwThreshold"` // NSFW 判定阈值
|
||||
CreatedAt time.Time `xorm:"created 'created_at'" json:"createdAt"` // 首次入库时间
|
||||
UpdatedAt time.Time `xorm:"updated 'updated_at'" json:"updatedAt"` // 最近更新时间
|
||||
}
|
||||
|
||||
// Store 数据库访问的封装。整个程序共用一个实例,通过它读写数据库。
|
||||
@@ -63,18 +65,6 @@ type AppConfig struct {
|
||||
Value string `xorm:"text 'value'"` // 配置值
|
||||
}
|
||||
|
||||
// NsfwRecord 一条"色情图片识别"的记录:哪张图、检测分多少、判了没有、现在在哪。
|
||||
// 前端「色情识别」页面就是查这张表来展示每张图片的检测结果,方便人工复核准确性。
|
||||
type NsfwRecord struct {
|
||||
Id int64 `xorm:"pk autoincr 'id'" json:"id"` // 自增主键
|
||||
FilePath string `xorm:"text 'file_path'" json:"filePath"` // 检测时的源路径(在哪发现的)
|
||||
FileName string `xorm:"varchar(255) 'file_name'" json:"fileName"` // 文件名(含扩展名)
|
||||
Score float64 `xorm:"real 'score'" json:"score"` // 命中的敏感部位最高分 0~1
|
||||
Threshold float64 `xorm:"real 'threshold'" json:"threshold"` // 判定阈值(>= 它才算色情)
|
||||
MovedPath string `xorm:"text 'moved_path'" json:"movedPath"` // 整理后实际位置(预览用);没搬走时同源路径
|
||||
CreatedAt time.Time `xorm:"created 'created_at'" json:"createdAt"` // 检测时间
|
||||
}
|
||||
|
||||
// ScanLog 一条"续传"记录:某个源文件上次被归类到了哪个目录。
|
||||
// 用途:扫描中断/出错后重跑时,已经归类过的文件直接复用上次的目标目录,
|
||||
// 不再重新探测尺寸 / 算 MD5 / 分析明暗 / 色情识别,省下几小时的重扫时间。
|
||||
@@ -106,8 +96,8 @@ func Open(dbPath string) (*Store, error) {
|
||||
_ = e.Close()
|
||||
return nil, err
|
||||
}
|
||||
// 自动创建/补全四张表(媒体记录表、配置表、色情识别记录表、续传表)
|
||||
if err := e.Sync(new(MediaInfo), new(AppConfig), new(NsfwRecord), new(ScanLog)); err != nil {
|
||||
// 自动创建/补全四张表(媒体记录表、配置表、续传表)
|
||||
if err := e.Sync(new(MediaInfo), new(AppConfig), new(ScanLog)); err != nil {
|
||||
_ = e.Close()
|
||||
return nil, err
|
||||
}
|
||||
@@ -157,44 +147,6 @@ func (s *Store) GetConfig(key string) string {
|
||||
return cfg.Value
|
||||
}
|
||||
|
||||
// ============================ 色情识别记录 ============================
|
||||
|
||||
// SaveNsfwBatch 批量保存色情识别记录(一次扫描攒一批,最后统一写入)。
|
||||
// 一次扫描可能命中几千张,一条 INSERT 塞不下(SQLite 单条语句最多 999 个变量),
|
||||
// 所以按 chunkSize 一组一组插,外面套一个事务保证要么全成功要么全回滚。
|
||||
func (s *Store) SaveNsfwBatch(records []*NsfwRecord) error {
|
||||
if s == nil || s.engine == nil || len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
const chunkSize = 100
|
||||
sess := s.engine.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < len(records); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(records) {
|
||||
end = len(records)
|
||||
}
|
||||
if _, err := sess.Insert(records[i:end]); err != nil {
|
||||
_ = sess.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// ClearNsfw 清空色情识别记录。每次开启色情识别的扫描开始时调用,
|
||||
// 保证「色情识别」页面看到的是最近一次扫描的结果。
|
||||
func (s *Store) ClearNsfw() error {
|
||||
if s == nil || s.engine == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := s.engine.Where("1 = 1").Delete(new(NsfwRecord))
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================ 续传表 ============================
|
||||
|
||||
// LoadScanLog 把全部续传记录读出来,按源路径建索引,供扫描时快速判断"这个文件上次归到哪了"。
|
||||
@@ -237,16 +189,6 @@ func (s *Store) SaveScanLogs(entries []ScanLog) error {
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// ListNsfw 查全部色情识别记录,按检测分从高到低排(方便先看最"实锤"的)。
|
||||
func (s *Store) ListNsfw() ([]NsfwRecord, error) {
|
||||
if s == nil || s.engine == nil {
|
||||
return nil, nil
|
||||
}
|
||||
list := make([]NsfwRecord, 0)
|
||||
err := s.engine.Desc("score").Find(&list)
|
||||
return list, err
|
||||
}
|
||||
|
||||
// ============================ 媒体记录的新增/更新 ============================
|
||||
|
||||
// SaveMedia 保存/更新一条媒体记录:按 MD5 找,有就更新,没有就新增。
|
||||
@@ -339,6 +281,7 @@ type MediaQuery struct {
|
||||
Codec string // 编码
|
||||
Format string // 容器格式
|
||||
Resolution string // 分辨率档位(如 1080P / 5K / 普通 / 原图)
|
||||
NsfwOnly bool // 只看色情图片(nsfw_score >= nsfw_threshold 且 threshold > 0)
|
||||
Page int // 第几页(从 1 开始)
|
||||
PageSize int // 每页几条
|
||||
SortField string // 按哪个字段排序:id / fileSize / width / duration / createdAt
|
||||
@@ -437,6 +380,10 @@ func buildMediaCond(q MediaQuery) (string, []interface{}) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 色情筛选:只看检测分 >= 阈值的图片
|
||||
if q.NsfwOnly {
|
||||
appendCond("nsfw_score >= nsfw_threshold AND nsfw_threshold > 0")
|
||||
}
|
||||
if cond == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user