refactor: 合并 NSFW 数据到 media_info 表

This commit is contained in:
2025-07-26 21:05:51 +08:00
parent 95e6297076
commit 59b0206cf1
12 changed files with 159 additions and 463 deletions
+36
View File
@@ -6,6 +6,9 @@ package api
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/dustin/go-humanize"
"github.com/gin-gonic/gin"
@@ -34,6 +37,7 @@ type mediaListReq struct {
Codec string `json:"codec"`
Format string `json:"format"`
Resolution string `json:"resolution"`
NsfwOnly interface{} `json:"nsfwOnly"`
SortField string `json:"sortField"`
SortOrder string `json:"sortOrder"`
}
@@ -65,6 +69,7 @@ func (c *MediaController) List(ctx *gin.Context) {
Codec: req.Codec,
Format: req.Format,
Resolution: req.Resolution,
NsfwOnly: req.NsfwOnly == true, // interface{} 转 bool:只有明确的 true 才算
Page: req.Page,
PageSize: req.PageSize,
SortField: req.SortField,
@@ -101,6 +106,37 @@ func (c *MediaController) Stats(ctx *gin.Context) {
Success(ctx, st)
}
// Preview 处理 GET /api/media/preview?path=xxx:预览图片文件。
// 只允许预览图片类型(jpeg/jpg/png/gif/webp/bmp),其他类型返回 403。
func (c *MediaController) Preview(ctx *gin.Context) {
path := strings.TrimSpace(ctx.Query("path"))
if path == "" {
Error(ctx, http.StatusBadRequest, "缺少 path 参数")
return
}
// 检查文件是否存在
info, err := os.Stat(path)
if err != nil {
Error(ctx, http.StatusNotFound, "文件不存在: "+path)
return
}
// 只允许预览图片
ext := strings.ToLower(filepath.Ext(info.Name()))
allowedExts := map[string]bool{
".jpg": true, ".jpeg": true, ".png": true,
".gif": true, ".webp": true, ".bmp": true,
}
if !allowedExts[ext] {
Error(ctx, http.StatusForbidden, "仅支持预览图片文件")
return
}
// 返回图片文件
ctx.File(path)
}
// toMediaView 给一条数据库记录补上展示用的派生字段。
func toMediaView(m store.MediaInfo) mediaView {
v := mediaView{MediaInfo: m}
-87
View File
@@ -1,87 +0,0 @@
// 色情识别相关的两个接口:查记录、看图片预览。
// 前端「色情识别」页面用它展示每次扫描判出的色情图片(检测分 + 路径 + 预览),
// 方便用户核对识别结果准不准。只读数据库 + 读本地图片文件,不干别的活。
package api
import (
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"kenaito-media-picker/internal/config"
"kenaito-media-picker/internal/store"
)
// nsfwDetectorReadyKey 记录"最近一次扫描时色情检测服务是否真的起来了"(存 AppConfig)。
// 配置开了但检测器没起来(没装 python/模型下载失败等)也能区分出来,方便前端提示。
// 由 scan 流程在服务启动成功/失败时写 1/0。
const nsfwDetectorReadyKey = "nsfw.detectorReady"
// NsfwController 色情识别相关接口的"窗口"。
type NsfwController struct {
store *store.Store // 数据库(只读)
}
// NewNsfwController 创建色情识别控制器,注入数据库。
func NewNsfwController(st *store.Store) *NsfwController {
return &NsfwController{store: st}
}
// nsfwView 列表里展示的一条记录:路径用"整理后位置",没有就用源路径。
type nsfwView struct {
Id int64 `json:"id"` // 主键
FileName string `json:"fileName"` // 文件名
Score float64 `json:"score"` // 检测分 0~1
Threshold float64 `json:"threshold"` // 判定阈值
Path string `json:"path"` // 预览/打开用的完整路径
CreatedAt string `json:"createdAt"` // 检测时间
}
// List 处理 GET /api/nsfw/list:查全部色情识别记录(按检测分从高到低)。
func (c *NsfwController) List(ctx *gin.Context) {
list, err := c.store.ListNsfw()
if err != nil {
Error(ctx, http.StatusInternalServerError, "查询色情识别记录失败:"+err.Error())
return
}
views := make([]nsfwView, 0, len(list))
for _, r := range list {
path := r.MovedPath
if path == "" {
path = r.FilePath
}
views = append(views, nsfwView{
Id: r.Id,
FileName: r.FileName,
Score: r.Score,
Threshold: r.Threshold,
Path: path,
CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
Success(ctx, map[string]interface{}{
"list": views,
"total": len(views),
"nsfwEnabled": config.GetAppConfig().NSFWEnabled,
// 没扫描过(空)或上次成功(1)都算可用;只有明确记过失败(0)才提示检测服务没起来
"nsfwDetector": c.store == nil || c.store.GetConfig(nsfwDetectorReadyKey) != "0",
})
}
// Preview 处理 GET /api/nsfw/preview?path=xxx:把指定图片文件返回给浏览器做预览。
// 这是本地单机工具,直接按路径读文件;文件不存在/已被移动时返回 404。
func (c *NsfwController) Preview(ctx *gin.Context) {
path := strings.TrimSpace(ctx.Query("path"))
if path == "" {
Error(ctx, http.StatusBadRequest, "缺少 path 参数")
return
}
if _, err := os.Stat(path); err != nil {
Error(ctx, http.StatusNotFound, "图片不存在或已被移动: "+path)
return
}
// 交给 gin 读文件并带上正确的 Content-Type / 断点续传
ctx.File(path)
}
+1 -5
View File
@@ -32,11 +32,7 @@ func NewRouter(st *store.Store) *gin.Engine {
mc := NewMediaController(st)
apiGroup.POST("/media/list", mc.List)
apiGroup.POST("/media/stats", mc.Stats)
// 色情识别(查看检测分 + 图片预览)
nc := NewNsfwController(st)
apiGroup.GET("/nsfw/list", nc.List)
apiGroup.GET("/nsfw/preview", nc.Preview)
apiGroup.GET("/media/preview", mc.Preview)
// 扫描整理(开始 / 中止 / 查进度 / 实时进度)
sc := NewScanController(service.NewScanService(st))