// Package classify 负责给媒体文件定"分辨率档位",决定它该进哪个分类文件夹。 // // 大白话理解,就是定分类命名规则: // - 画面是横的还是竖的?(横屏 / 竖屏 / 正方形) // - 清晰度算哪个档?(720P / 1080P / 4K…;不标准的按长边大小给个约数,如 5K) // - 图片还要看是暗调还是亮调(dark_25 / dark_50 / dark_75),以及色情评分档(安全 / 低俗 / 擦边 / 色情) // // 最终拼出一个文件夹名,比如: // - 「图片_横屏_1080P-dark_25-安全」 一张横向、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-dark_25-安全」:普通图片 = 方向 + 清晰度 + 明暗 + 色情评分档 func ImageDir(ext string, width, height int, darkScore, lightScore, nsfwScore float64, nsfwEnabled bool) string { if strings.EqualFold(ext, ".gif") { return "图片_GIF" } dir := resolutionDir("图片", ext, width, height) result := dir + "-" + BrightnessName(darkScore, lightScore) if nsfwEnabled { result += "-" + NsfwLevelName(nsfwScore) } return result } // 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 }