feat: 断点续传与色情识别提速/修复,支持 Linux 交叉编译
- feat: 新增续传表 scan_log,重扫跳过已归类文件(尺寸/头尾指纹校验) - fix: 色情识别记录批量入库超 SQLite 999 变量上限,改每 100 条一批 - perf: NudeNet 检测改为多线程推理池(每线程独立模型实例) - perf: 明暗分析 JPEG(YCbCr) 快路径;SQLite 读并发放开;加阶段耗时统计 - fix: 前端色情识别状态由后端如实上报(配置开关+检测服务是否起来) - feat: ffprobe 内嵌按平台拆分,Linux 构建回退系统 ffprobe,支持交叉编译 - docs: README 补充断点续传与 CentOS 交叉编译步骤 - chore: organize.workers 默认 8
This commit is contained in:
+87
-6
@@ -75,6 +75,20 @@ type NsfwRecord struct {
|
||||
CreatedAt time.Time `xorm:"created 'created_at'" json:"createdAt"` // 检测时间
|
||||
}
|
||||
|
||||
// ScanLog 一条"续传"记录:某个源文件上次被归类到了哪个目录。
|
||||
// 用途:扫描中断/出错后重跑时,已经归类过的文件直接复用上次的目标目录,
|
||||
// 不再重新探测尺寸 / 算 MD5 / 分析明暗 / 色情识别,省下几小时的重扫时间。
|
||||
// 大文件记录头尾指纹,重扫时核对内容没变才放心跳过。
|
||||
type ScanLog struct {
|
||||
Id int64 `xorm:"pk autoincr 'id'" json:"id"`
|
||||
SourcePath string `xorm:"varchar(1024) unique 'source_path'" json:"sourcePath"` // 源文件路径(唯一)
|
||||
TargetDir string `xorm:"text 'target_dir'" json:"targetDir"` // 归类结果:目标目录
|
||||
FileSize int64 `xorm:"bigint 'file_size'" json:"fileSize"` // 文件大小(校验内容是否变化)
|
||||
HeadMd5 string `xorm:"char(32) 'head_md5'" json:"headMd5"` // 文件头 256KB MD5(大文件指纹)
|
||||
TailMd5 string `xorm:"char(32) 'tail_md5'" json:"tailMd5"` // 文件尾 256KB MD5(大文件指纹)
|
||||
UpdatedAt time.Time `xorm:"updated 'updated_at'" json:"updatedAt"` // 最近归类时间
|
||||
}
|
||||
|
||||
// Open 打开(或第一次时创建)数据库,并保证表结构齐全。
|
||||
func Open(dbPath string) (*Store, error) {
|
||||
e, err := xorm.NewEngine("sqlite3", dbPath)
|
||||
@@ -82,10 +96,18 @@ func Open(dbPath string) (*Store, error) {
|
||||
return nil, err
|
||||
}
|
||||
e.ShowSQL(false)
|
||||
// SQLite 同一时刻只允许一个写连接,这里限制成 1 个,避免并发写冲突
|
||||
e.SetMaxOpenConns(1)
|
||||
// 自动创建/补全三张表(媒体记录表、配置表、色情识别记录表)
|
||||
if err := e.Sync(new(MediaInfo), new(AppConfig), new(NsfwRecord)); err != nil {
|
||||
// SQLite 同一时刻只允许一个写连接。这里把读连接放开几路:
|
||||
// 扫描探测阶段是并发读(查指纹),被 1 个连接串行化会拖慢并行;
|
||||
// 写仍走单事务(批量入库),由 SQLite 自己的锁机制串行,不会冲突。
|
||||
e.SetMaxOpenConns(8)
|
||||
// 给数据库加个忙等待,万一遇到并发写也不至于立刻报 SQLITE_BUSY
|
||||
e.SetMaxIdleConns(8)
|
||||
if _, err := e.Exec("PRAGMA busy_timeout = 10000"); err != nil {
|
||||
_ = e.Close()
|
||||
return nil, err
|
||||
}
|
||||
// 自动创建/补全四张表(媒体记录表、配置表、色情识别记录表、续传表)
|
||||
if err := e.Sync(new(MediaInfo), new(AppConfig), new(NsfwRecord), new(ScanLog)); err != nil {
|
||||
_ = e.Close()
|
||||
return nil, err
|
||||
}
|
||||
@@ -138,12 +160,29 @@ func (s *Store) GetConfig(key string) string {
|
||||
// ============================ 色情识别记录 ============================
|
||||
|
||||
// SaveNsfwBatch 批量保存色情识别记录(一次扫描攒一批,最后统一写入)。
|
||||
// 一次扫描可能命中几千张,一条 INSERT 塞不下(SQLite 单条语句最多 999 个变量),
|
||||
// 所以按 chunkSize 一组一组插,外面套一个事务保证要么全成功要么全回滚。
|
||||
func (s *Store) SaveNsfwBatch(records []*NsfwRecord) error {
|
||||
if s == nil || s.engine == nil || len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := s.engine.Insert(records)
|
||||
return err
|
||||
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 清空色情识别记录。每次开启色情识别的扫描开始时调用,
|
||||
@@ -156,6 +195,48 @@ func (s *Store) ClearNsfw() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================ 续传表 ============================
|
||||
|
||||
// LoadScanLog 把全部续传记录读出来,按源路径建索引,供扫描时快速判断"这个文件上次归到哪了"。
|
||||
func (s *Store) LoadScanLog() (map[string]ScanLog, error) {
|
||||
m := make(map[string]ScanLog)
|
||||
if s == nil || s.engine == nil {
|
||||
return m, nil
|
||||
}
|
||||
list := make([]ScanLog, 0)
|
||||
if err := s.engine.Find(&list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, e := range list {
|
||||
m[e.SourcePath] = e
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// SaveScanLogs 批量保存续传记录(按源路径 upsert)。攒一批一起写,少开事务。
|
||||
// 单行删除 + 单行插入,没有 999 变量的限制,一批全在一个事务里。
|
||||
func (s *Store) SaveScanLogs(entries []ScanLog) error {
|
||||
if s == nil || s.engine == nil || len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
sess := s.engine.NewSession()
|
||||
defer sess.Close()
|
||||
if err := sess.Begin(); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if _, err := sess.Where("source_path = ?", e.SourcePath).Delete(new(ScanLog)); err != nil {
|
||||
_ = sess.Rollback()
|
||||
return err
|
||||
}
|
||||
if _, err := sess.Insert(&e); err != nil {
|
||||
_ = sess.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
// ListNsfw 查全部色情识别记录,按检测分从高到低排(方便先看最"实锤"的)。
|
||||
func (s *Store) ListNsfw() ([]NsfwRecord, error) {
|
||||
if s == nil || s.engine == nil {
|
||||
|
||||
Reference in New Issue
Block a user