- 重构后端与前端,全程大白话注释(不懂程序也能看懂) - 移动阶段改为全局并行(MoveAll,工人数=CPU核心数) - 去掉记录每个文件搬去哪了(删除去向回填,解决写库卡顿) - 去掉重复文件标记与统计:重复判定改为 MD5+路径都相同才算,重复则原地忽略不移动;MD5 相同但路径不同仍照常搬 - 媒体库去掉归类目录列,看板去掉重复/归类分布统计
132 lines
4.0 KiB
Go
132 lines
4.0 KiB
Go
// 媒体库的两个接口:查列表、查统计。
|
||
// 「媒体库」页面查列表(带筛选和分页),「统计看板」页面查统计。
|
||
// 这两个接口只读数据库,不干别的活。
|
||
package api
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"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"`
|
||
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,
|
||
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)
|
||
}
|
||
|
||
// 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)
|
||
}
|