Files
kenaito-media-picker/internal/classify/classify.go
T
odboy 8979c744a8 refactor: 全面重构代码并加白话注释,移动全局并行,去掉去向/重复记录
- 重构后端与前端,全程大白话注释(不懂程序也能看懂)
- 移动阶段改为全局并行(MoveAll,工人数=CPU核心数)
- 去掉记录每个文件搬去哪了(删除去向回填,解决写库卡顿)
- 去掉重复文件标记与统计:重复判定改为 MD5+路径都相同才算,重复则原地忽略不移动;MD5 相同但路径不同仍照常搬
- 媒体库去掉归类目录列,看板去掉重复/归类分布统计
2025-05-11 21:05:35 +08:00

156 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package classify 负责给媒体文件定"分辨率档位",决定它该进哪个分类文件夹。
//
// 大白话理解,就是定分类命名规则:
// - 画面是横的还是竖的?(横屏 / 竖屏 / 正方形)
// - 清晰度算哪个档?(720P / 1080P / 4K…;不标准的按长边大小给个约数,如 5K)
// - 图片还要看是暗调还是亮调(dark / mid / light)
//
// 最终拼出一个文件夹名,比如:
// - 「图片_横屏_1080P-light」 一张横向、1080P、偏亮的图片
// - 「视频_竖屏_4k」 一个竖向、4K 的视频
//
// 这个包全是"纯函数"——不依赖任何状态,给什么算什么,好理解也好测。
package classify
import (
"fmt"
"strings"
)
// ============================ 画面方向 ============================
// Orientation 画面方向。
type Orientation int
const (
OrientationSquare Orientation = iota // 正方形(宽 = 高)
OrientationHorizontal // 横屏(宽 > 高)
OrientationVertical // 竖屏(宽 < 高)
)
// OrientationOf 根据宽高判断画面是横是竖还是正方。
func OrientationOf(width, height int) Orientation {
switch {
case width > height:
return OrientationHorizontal
case width < height:
return OrientationVertical
default:
return OrientationSquare
}
}
// String 把方向转成中文名,方便拼进文件夹名和前端展示。
func (o Orientation) String() string {
switch o {
case OrientationHorizontal:
return "横屏"
case OrientationVertical:
return "竖屏"
default:
return "等比"
}
}
// ============================ 归类目录名 ============================
// ImageDir 返回一张图片该进的文件夹名,比如:
// - 「图片_GIF」:GIF 动图单独放(不做明暗分类)
// - 「图片_横屏_1080P-light」:普通图片 = 方向 + 清晰度 + 明暗
func ImageDir(ext string, width, height int, darkScore, lightScore float64) string {
if strings.EqualFold(ext, ".gif") {
return "图片_GIF"
}
dir := resolutionDir("图片", ext, width, height)
return dir + "-" + BrightnessName(darkScore, lightScore)
}
// 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 = "原画质"
}
return kind + "_" + ori + "_" + levelName(longEdge(width, height), overflow)
}
// longEdge 返回宽和高中较长的那条边(像素)。
func longEdge(w, h int) int {
if w > h {
return w
}
return h
}
// ============================ 分辨率档位(给前端筛选/统计用) ============================
// 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 ""
}
if suffix := matchStandard(width, height); suffix != "" {
return suffix
}
return levelName(longEdge(width, height), "原图")
}
// 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 {
return level * bucketStep, (level + 1) * bucketStep, 0, 0
}
}
return -1, -1, 0, 0
}