168 lines
5.0 KiB
Go
168 lines
5.0 KiB
Go
// 媒体库的两个接口:查列表、查统计。
|
||
// 「媒体库」页面查列表(带筛选和分页),「统计看板」页面查统计。
|
||
// 这两个接口只读数据库,不干别的活。
|
||
package api
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/dustin/go-humanize"
|
||
"github.com/gin-gonic/gin"
|
||
|
||
"kenaito-media-picker/internal/classify"
|
||
"kenaito-media-picker/internal/store"
|
||
)
|
||
|
||
// MediaController 媒体库相关接口的"窗口"。
|
||
type MediaController struct {
|
||
store *store.Store // 数据库(只读)
|
||
}
|
||
|
||
// NewMediaController 创建媒体库控制器,注入数据库。
|
||
func NewMediaController(st *store.Store) *MediaController {
|
||
return &MediaController{store: st}
|
||
}
|
||
|
||
// mediaListReq 媒体库列表的查询条件(前端「媒体库」页面传过来的)。
|
||
type mediaListReq struct {
|
||
Page int `json:"page"`
|
||
PageSize int `json:"pageSize"`
|
||
MediaType string `json:"mediaType"`
|
||
Orientation string `json:"orientation"`
|
||
Keyword string `json:"keyword"`
|
||
Codec string `json:"codec"`
|
||
Format string `json:"format"`
|
||
Resolution string `json:"resolution"`
|
||
NsfwOnly interface{} `json:"nsfwOnly"`
|
||
SortField string `json:"sortField"`
|
||
SortOrder string `json:"sortOrder"`
|
||
}
|
||
|
||
// mediaView 列表里展示的一条记录:在数据库记录的基础上,
|
||
// 再补几个"现算"的字段(方向、分辨率档位、人类可读的体积和时长),方便页面直接显示。
|
||
type mediaView struct {
|
||
store.MediaInfo
|
||
Orientation string `json:"orientation"` // 横屏/竖屏/等比
|
||
Resolution string `json:"resolution"` // 分辨率档位(如 1080P)
|
||
SizeText string `json:"sizeText"` // 人类可读体积(如 1.5 MB)
|
||
DurationText string `json:"durationText"` // 人类可读时长(如 05:30)
|
||
}
|
||
|
||
// List 处理 POST /api/media/list:按条件分页查媒体库列表。
|
||
func (c *MediaController) List(ctx *gin.Context) {
|
||
var req mediaListReq
|
||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||
Error(ctx, http.StatusBadRequest, "请求参数错误:"+err.Error())
|
||
return
|
||
}
|
||
if req.Page < 1 {
|
||
req.Page = 1
|
||
}
|
||
q := store.MediaQuery{
|
||
MediaType: req.MediaType,
|
||
Orientation: req.Orientation,
|
||
Keyword: req.Keyword,
|
||
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,
|
||
SortOrder: req.SortOrder,
|
||
}
|
||
|
||
// 查这一页的数据,同时查总条数(分页条要用)
|
||
list, err := c.store.ListMedia(q)
|
||
if err != nil {
|
||
Error(ctx, http.StatusInternalServerError, "查询媒体列表失败:"+err.Error())
|
||
return
|
||
}
|
||
total, err := c.store.CountMedia(q)
|
||
if err != nil {
|
||
Error(ctx, http.StatusInternalServerError, "统计媒体数量失败:"+err.Error())
|
||
return
|
||
}
|
||
|
||
// 每条记录补上"现算"的展示字段再返回
|
||
views := make([]interface{}, 0, len(list))
|
||
for _, m := range list {
|
||
views = append(views, toMediaView(m))
|
||
}
|
||
Success(ctx, map[string]interface{}{"list": views, "total": total})
|
||
}
|
||
|
||
// Stats 处理 POST /api/media/stats:统计看板要用的全部数据。
|
||
func (c *MediaController) Stats(ctx *gin.Context) {
|
||
st, err := c.store.Stats()
|
||
if err != nil {
|
||
Error(ctx, http.StatusInternalServerError, "统计失败:"+err.Error())
|
||
return
|
||
}
|
||
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}
|
||
// 宽高有效才算方向和分辨率档位
|
||
if m.Width > 0 && m.Height > 0 {
|
||
v.Orientation = classify.OrientationOf(m.Width, m.Height).String()
|
||
v.Resolution = classify.ResLabel(m.Width, m.Height)
|
||
}
|
||
if m.FileSize > 0 {
|
||
v.SizeText = humanize.Bytes(uint64(m.FileSize))
|
||
}
|
||
if m.Duration > 0 {
|
||
v.DurationText = formatDuration(m.Duration)
|
||
}
|
||
return v
|
||
}
|
||
|
||
// formatDuration 把秒数转成人类可读的时:分:秒。
|
||
func formatDuration(sec float64) string {
|
||
s := int(sec)
|
||
h := s / 3600
|
||
m := (s % 3600) / 60
|
||
secs := s % 60
|
||
if h > 0 {
|
||
return fmt.Sprintf("%d:%02d:%02d", h, m, secs)
|
||
}
|
||
return fmt.Sprintf("%02d:%02d", m, secs)
|
||
}
|