refactor: 全面重构代码并加白话注释,移动全局并行,去掉去向/重复记录

- 重构后端与前端,全程大白话注释(不懂程序也能看懂)
- 移动阶段改为全局并行(MoveAll,工人数=CPU核心数)
- 去掉记录每个文件搬去哪了(删除去向回填,解决写库卡顿)
- 去掉重复文件标记与统计:重复判定改为 MD5+路径都相同才算,重复则原地忽略不移动;MD5 相同但路径不同仍照常搬
- 媒体库去掉归类目录列,看板去掉重复/归类分布统计
This commit is contained in:
2025-05-11 21:05:35 +08:00
parent f915a9b1ac
commit 8979c744a8
26 changed files with 1044 additions and 683 deletions
+54 -15
View File
@@ -1,4 +1,15 @@
// Package classify 提供纯函数的分辨率归类规则,不依赖任何状态。
// Package classify 负责给媒体文件定"分辨率档位",决定它该进哪个分类文件夹。
//
// 大白话理解,就是定分类命名规则:
// - 画面是横的还是竖的?(横屏 / 竖屏 / 正方形)
// - 清晰度算哪个档?(720P / 1080P / 4K…;不标准的按长边大小给个约数,如 5K)
// - 图片还要看是暗调还是亮调(dark / mid / light)
//
// 最终拼出一个文件夹名,比如:
// - 「图片_横屏_1080P-light」 一张横向、1080P、偏亮的图片
// - 「视频_竖屏_4k」 一个竖向、4K 的视频
//
// 这个包全是"纯函数"——不依赖任何状态,给什么算什么,好理解也好测。
package classify
import (
@@ -6,16 +17,18 @@ import (
"strings"
)
// Orientation 画面方向
// ============================ 画面方向 ============================
// Orientation 画面方向。
type Orientation int
const (
OrientationSquare Orientation = iota
OrientationHorizontal
OrientationVertical
OrientationSquare Orientation = iota // 正方形(宽 = 高)
OrientationHorizontal // 横屏(宽 > 高)
OrientationVertical // 竖屏(宽 < 高)
)
// OrientationOf 由宽高判定画面方向
// OrientationOf 根据宽高判断画面是横是竖还是正方。
func OrientationOf(width, height int) Orientation {
switch {
case width > height:
@@ -27,6 +40,7 @@ func OrientationOf(width, height int) Orientation {
}
}
// String 把方向转成中文名,方便拼进文件夹名和前端展示。
func (o Orientation) String() string {
switch o {
case OrientationHorizontal:
@@ -38,8 +52,11 @@ func (o Orientation) String() string {
}
}
// ImageDir 返回图片归类子目录名,如 "图片_横屏_1080P-dark_70" / "图片_横屏_1080P-mid" / "图片_GIF"。
// darkScore/lightScore 用于明暗分类(GIF 不做明暗分类,忽略这两项)。
// ============================ 归类目录名 ============================
// ImageDir 返回一张图片该进的文件夹名,比如:
// - 「图片_GIF」:GIF 动图单独放(不做明暗分类)
// - 「图片_横屏_1080P-light」:普通图片 = 方向 + 清晰度 + 明暗
func ImageDir(ext string, width, height int, darkScore, lightScore float64) string {
if strings.EqualFold(ext, ".gif") {
return "图片_GIF"
@@ -48,19 +65,25 @@ func ImageDir(ext string, width, height int, darkScore, lightScore float64) stri
return dir + "-" + BrightnessName(darkScore, lightScore)
}
// VideoDir 返回视频归类子目录名,如 "视频_竖屏_4k" / "视频_横屏_GIF"
// VideoDir 返回一个视频该进的文件夹名,比如「视频_竖屏_4k」「视频_横屏_GIF」。
func VideoDir(ext string, width, height int) string {
return resolutionDir("视频", ext, width, height)
}
// resolutionDir 拼出"类型 + 方向 + 清晰度"这段目录名。
// 步骤:先看宽高是横是竖;再匹配标准清晰度(720P/1080P…),
// 匹配不上就按长边大小给个约数(普通 / 5K / 原画质)。
func resolutionDir(kind, ext string, width, height int) string {
ori := OrientationOf(width, height).String()
// GIF 特殊:不管多清楚,都归「_GIF」
if strings.EqualFold(ext, ".gif") {
return kind + "_" + ori + "_GIF"
}
// 先看是不是标准清晰度(精确匹配宽高)
if suffix := matchStandard(width, height); suffix != "" {
return kind + "_" + ori + "_" + suffix
}
// 不是标准清晰度:按长边给约数。图片叫"原图",视频叫"原画质"(超出档位时用)
overflow := "原图"
if kind == "视频" {
overflow = "原画质"
@@ -68,7 +91,7 @@ func resolutionDir(kind, ext string, width, height int) string {
return kind + "_" + ori + "_" + levelName(longEdge(width, height), overflow)
}
// longEdge 返回长边像素
// longEdge 返回宽和高中较长的那条边(像素)。
func longEdge(w, h int) int {
if w > h {
return w
@@ -76,8 +99,16 @@ func longEdge(w, h int) int {
return h
}
// ResLabel 返回分辨率档位标签(标准后缀 / 1K~19K / 普通 / 原图),无有效尺寸时返回空串。
// 供媒体库统计与筛选使用,图片/视频统一以"原图"作为超档标签。
// ============================ 分辨率档位(给前端筛选/统计用) ============================
// ResLabel 把一个宽高翻译成"分辨率档位标签",例如:
// - 1920×1080 → "1080P"(标准档直接叫标准名)
// - 2560×1440 → "2D5KP"
// - 1024×768 → "普通"
// - 5120×2880 → "5K"(约数)
// - 20000×10000 → "原图"(超出档位上限)
//
// 图片、视频统一用"原图"作为超出档位的标签。宽高无效时返回空串。
func ResLabel(width, height int) string {
if width <= 0 || height <= 0 {
return ""
@@ -88,24 +119,32 @@ func ResLabel(width, height int) string {
return levelName(longEdge(width, height), "原图")
}
// ResRange 解析分辨率档位标签,返回 (长边下限 minLong, 长边上限 maxLong(不含), 精确宽 exactW, 精确高 exactH)。
// minLong<0 表示未知标签;maxLong 为 -1 表示无上限;exactW/exactH 非零时表示标准分辨率精确匹配。
// 供数据层构造 SQL 过滤条件,规则与归类逻辑保持一致。
// ResRange 把"分辨率档位标签"翻译回具体的尺寸范围,给数据库查 SQL 用。
// 返回四样东西:
// - minLong:长边至少多少像素(-1 表示这个标签不认得)
// - maxLong:长边小于多少像素(-1 表示没有上限)
// - exactW / exactH:标准分辨率的精确宽高(非零表示要精确匹配,不用管范围)
//
// 规则和归类逻辑必须保持一致,否则筛选出来的和实际分类会对不上。
func ResRange(label string) (minLong, maxLong, exactW, exactH int) {
if label == "" {
return -1, -1, 0, 0
}
// 标准档:精确匹配宽高
for _, r := range standardResolutions {
if r.suffix == label {
return 0, 0, r.width, r.height
}
}
// 「普通」:长边不到 1 档(<1024px)
if label == "普通" {
return 0, bucketStep, 0, 0
}
// 「原图」:长边超出最大档(≥20480px),没有上限
if label == "原图" {
return (maxLevel + 1) * bucketStep, -1, 0, 0
}
// 「NK」档(如 5K):长边在第 N 档到第 N+1 档之间
if len(label) > 1 && label[len(label)-1] == 'K' {
var level int
if _, err := fmt.Sscanf(label, "%dK", &level); err == nil && level >= 1 && level <= maxLevel {