175 lines
6.5 KiB
Go
175 lines
6.5 KiB
Go
// Package classify 负责给媒体文件定"分辨率档位",决定它该进哪个分类文件夹。
|
||
//
|
||
// 大白话理解,就是定分类命名规则:
|
||
// - 画面是横的还是竖的?(横屏 / 竖屏 / 正方形)
|
||
// - 清晰度算哪个档?(720P / 1080P / 4K…;不标准的按长边大小给个约数,如 5K)
|
||
// - 图片还要看是暗调还是亮调(dark / mid / bright),作为子目录
|
||
//
|
||
// 最终拼出一个文件夹名,比如:
|
||
// - 「图片_横屏_1080P/bright_20」 一张横向、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/bright_20」:普通图片 = 方向 + 清晰度,明暗作为其子目录
|
||
func ImageDir(ext string, width, height int, darkScore, lightScore float64) string {
|
||
return imageDir("图片", ext, width, height, darkScore, lightScore)
|
||
}
|
||
|
||
// ImageDirSensitive 返回一张被判为色情的图片该进的文件夹路径,比如:
|
||
// - 「图片_敏感_GIF」:GIF 单独放(不做明暗分类)
|
||
// - 「图片_敏感_横屏_1080P/bright_20」:普通分类前加「_敏感」前缀,明暗仍是子目录
|
||
func ImageDirSensitive(ext string, width, height int, darkScore, lightScore float64) string {
|
||
return imageDir("图片_敏感", ext, width, height, darkScore, lightScore)
|
||
}
|
||
|
||
// ImageDirHentai 返回一张被判为二次元敏感的图片该进的文件夹路径,比如:
|
||
// - 「图片_二次元_GIF」:GIF 单独放(不做明暗分类)
|
||
// - 「图片_二次元_横屏_1080P/bright_20」:普通分类前加「_二次元」前缀,明暗仍是子目录
|
||
func ImageDirHentai(ext string, width, height int, darkScore, lightScore float64) string {
|
||
return imageDir("图片_二次元", ext, width, height, darkScore, lightScore)
|
||
}
|
||
|
||
// imageDir 拼出"类型 + 方向 + 清晰度 + 明暗子目录"这段路径。
|
||
func imageDir(kind, ext string, width, height int, darkScore, lightScore float64) string {
|
||
if strings.EqualFold(ext, ".gif") {
|
||
return kind + "_GIF"
|
||
}
|
||
dir := resolutionDir(kind, 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
|
||
}
|