refactor: 按模块重构项目结构

- 删除 vars 全局状态,按职责拆分 internal/ 包:scanner/probe/classify/organize/mover/store/digest
- 数据驱动归类规则替代图片/视频重复分支,store 改为结构体实例
- build.sh 按系统类型编译:Windows 用 upx 压缩,macOS/Linux 不压缩
- 忽略目录拆分:图片_已忽略 / 视频_已忽略
- 输出构建产物加入 .gitignore
This commit is contained in:
2025-03-26 21:08:12 +08:00
parent 26cbd5bb27
commit 4da4b738e6
36 changed files with 1016 additions and 2476 deletions
+7 -2
View File
@@ -27,6 +27,11 @@
# Go workspace file # Go workspace file
go.work go.work
# 内嵌的 ffprobe 二进制(约 80~150MB),不入库,需自行放置于 vars/bin/ # 内嵌的 ffprobe 二进制(约 80~150MB),不入库,需自行放置于 internal/probe/bin/
vars/bin/ internal/probe/bin/
# 构建产物(build.sh 输出)
kenaito-media-picker
kenaito-media-picker.exe
kenaito-media-picker-release.exe
+13 -8
View File
@@ -31,9 +31,9 @@ sh build.sh
``` ```
- 依赖 Go 1.19+(`go.mod` 指定 1.19) - 依赖 Go 1.19+(`go.mod` 指定 1.19)
- 每个平台构建时只内嵌**本平台**的 ffprobe:`vars/bin/ffprobe_mac`(macOS)、`vars/bin/ffprobe.exe`(Windows),需提前放置(未入库,见 `.gitignore`) - 每个平台构建时只内嵌**本平台**的 ffprobe:`internal/probe/bin/ffprobe_mac`(macOS)、`internal/probe/bin/ffprobe.exe`(Windows),需提前放置(未入库,见 `.gitignore`)
- `build.sh` 使用 `upx` 压缩产物,输出 `kenaito-media-picker.exe`(Windows 单文件) - `build.sh` 按系统类型编译:**Windows** 下 `go build` 后使用 `upx` 压缩,输出 `kenaito-media-picker.exe`;**macOS / Linux** 直接构建,输出 `kenaito-media-picker`(不压缩)
- 编译所需工具(ffmpeg / upx)压缩包已放在 `doc/` 目录,按需解压使用 - 编译所需工具(ffmpeg / upx)压缩包已放在 `doc/` 目录,按需解压使用(upx 仅 Windows 构建时需要,放置于项目根目录 `upx`)
## 使用方法 ## 使用方法
@@ -87,7 +87,7 @@ kenaito-media-picker.exe
{uid}-视频_横屏_GIF / 视频_竖屏_GIF / 视频_等比_GIF {uid}-视频_横屏_GIF / 视频_竖屏_GIF / 视频_等比_GIF
音频 ← 所有音频文件(ffmpeg 可识别的格式) 音频 ← 所有音频文件(ffmpeg 可识别的格式)
视频_读取异常 ← mime 为视频但 ffprobe 读取失败 视频_读取异常 ← mime 为视频但 ffprobe 读取失败
已忽略 ← 非媒体文件或无法识别 视频_已忽略 ← 非媒体文件或无法识别
``` ```
> 视频时长与分辨率通过内嵌 ffprobe 读取(处理时会临时释放到当前目录、完成后自动删除);凡 ffmpeg 支持的视频/音频格式均可识别。 > 视频时长与分辨率通过内嵌 ffprobe 读取(处理时会临时释放到当前目录、完成后自动删除);凡 ffmpeg 支持的视频/音频格式均可识别。
@@ -95,10 +95,15 @@ kenaito-media-picker.exe
## 目录结构 ## 目录结构
```text ```text
├── main.go # 程序入口(扫描 → 过滤 → 图片/媒体处理) ├── main.go # 程序入口(装配与编排)
├── core/ # 核心逻辑:文件扫描、MIME 过滤、图片/视频/音频分组与移动 ├── internal/
├── util/ # 工具:MIME 识别、图片尺寸读取、ffprobe 封装、文件移动 │ ├── scanner/ # 文件扫描、MIME 读取、图片/媒体候选过滤
├── vars/ # 全局状态 + 按系统内嵌 ffprobe(go:embed,二进制在 vars/bin/) │ ├── probe/ # 媒体探测:图片尺寸 + ffprobe 解析 + 内嵌解码器释放
│ ├── classify/ # 纯函数归类规则(方向 + 标准分辨率 + K档)
│ ├── organize/ # 整理编排:探测→归类→收集→移动→记录去向
│ ├── mover/ # 文件移动(rename / 跨盘 copy+delete)
│ ├── store/ # SQLite 媒体信息持久化
│ └── digest/ # 文件 MD5 / 指纹采样
├── doc/ # 构建依赖(ffmpeg / upx 压缩包) ├── doc/ # 构建依赖(ffmpeg / upx 压缩包)
└── build.sh # 编译脚本(upx 压缩) └── build.sh # 编译脚本(upx 压缩)
``` ```
+23 -7
View File
@@ -1,7 +1,23 @@
rm -f kenaito-media-picker.exe #!/bin/sh
rm -f kenaito-media-picker-release.exe # 构建脚本:按系统类型选择编译方式
# build # Windows (MINGW/MSYS/CYGWIN): go build + upx 压缩 → kenaito-media-picker.exe
go build -o kenaito-media-picker-release.exe main.go # macOS / Linux: 直接构建,不压缩 → kenaito-media-picker
# upx compress
./upx -o kenaito-media-picker.exe kenaito-media-picker-release.exe set -e
rm -f kenaito-media-picker-release.exe
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*)
echo "=== Windows: go build + upx 压缩"
rm -f kenaito-media-picker.exe kenaito-media-picker-release.exe
go build -o kenaito-media-picker-release.exe .
./upx -o kenaito-media-picker.exe kenaito-media-picker-release.exe
rm -f kenaito-media-picker-release.exe
;;
*)
echo "=== 非 Windows (macOS/Linux): 直接构建,不压缩"
rm -f kenaito-media-picker
go build -o kenaito-media-picker .
;;
esac
echo "=== 构建完成"
-114
View File
@@ -1,114 +0,0 @@
package core
import (
"encoding/binary"
"fmt"
"io"
"os"
)
// BMPFileHeader represents the bitmap file header structure.
type BMPFileHeader struct {
FileType [2]byte
FileSize uint32
Reserved1 uint16
Reserved2 uint16
DataOffset uint32
}
// BMPInfoHeader represents the bitmap info header structure.
type BMPInfoHeader struct {
Size uint32
Width int32
Height int32
Planes uint16
BitsPerPixel uint16
Compression uint32
SizeImage uint32
XPelsPerMeter int32
YPelsPerMeter int32
ColorsUsed uint32
ColorsImportant uint32
}
func ReadBmpImage(filePath string) (error, int32, int32) {
file, err := os.Open(filePath)
if err != nil {
fmt.Printf("=== Failed to open file: %v\n", err)
return err, 0, 0
}
defer file.Close()
// Read the file header.
var fileHeader BMPFileHeader
if err := binary.Read(file, binary.LittleEndian, &fileHeader); err != nil {
fmt.Printf("=== Failed to Read file: %v\n", err)
return err, 0, 0
}
// Check if it's a valid BMP file by verifying the file type.
if string(fileHeader.FileType[:]) != "BM" {
return fmt.Errorf("not a valid BMP file: file type is %q, expected \"BM\"", string(fileHeader.FileType[:])), 0, 0
}
// Read the info header.
var infoHeader BMPInfoHeader
if err := binary.Read(file, binary.LittleEndian, &infoHeader); err != nil {
fmt.Printf("=== Failed to Read file: %v\n", err)
return err, 0, 0
}
// 自顶向下 BMP 的高度为负值,取绝对值避免影响后续解析
return err, absInt32(infoHeader.Width), absInt32(infoHeader.Height)
//fmt.Printf("=== File size: %d bytes\n", fileHeader.FileSize)
//fmt.Printf("=== Image dimensions: %dx%d\n", )
//fmt.Printf("=== Bits per pixel: %d\n", infoHeader.BitsPerPixel)
// At this point, you would typically read the pixel data into a slice,
// taking into account any padding required for alignment and the specific
// pixel format (e.g., RGB, RGBA, indexed color). This part is omitted here.
// You may also need to skip over a possible color palette if present.
// The size of the palette can be inferred from the number of colors used and
// the bits per pixel value.
// For simplicity, we'll just read the rest of the file as raw bytes.
//data, err := ioutil.ReadAll(file)
//if err != nil {
// panic(err)
//}
//fmt.Printf("=== Read %d bytes of pixel data.\n", len(data))
}
// absInt32 返回 int32 的绝对值
func absInt32(v int32) int32 {
if v < 0 {
return -v
}
return v
}
// ReadPsdImage 读取PSD/PSB格式的图片信息(两者共享相同的8BPS文件头)
func ReadPsdImage(filePath string) (err error, width int, height int) {
file, err := os.Open(filePath)
if err != nil {
fmt.Printf("=== Failed to open file: %v\n", err)
return err, 0, 0
}
defer file.Close()
var header [26]byte
if _, err := io.ReadFull(file, header[:]); err != nil {
fmt.Printf("=== Failed to read PSD/PSB header: %v\n", err)
return err, 0, 0
}
if string(header[0:4]) != "8BPS" {
return fmt.Errorf("not a valid PSD/PSB file: magic %q, expected \"8BPS\"", string(header[0:4])), 0, 0
}
version := binary.BigEndian.Uint16(header[4:6])
if version != 1 && version != 2 {
return fmt.Errorf("unsupported PSD/PSB version: %d", version), 0, 0
}
height = int(binary.BigEndian.Uint32(header[14:18]))
width = int(binary.BigEndian.Uint32(header[18:22]))
return nil, width, height
}
-100
View File
@@ -1,100 +0,0 @@
package core
import (
"fmt"
"github.com/redmask-hb/GoSimplePrint/goPrint"
"kenaito-media-picker/store"
"kenaito-media-picker/util"
"kenaito-media-picker/vars"
"log"
"os"
"path"
"path/filepath"
"strings"
)
type FileScanner struct {
}
func (FileScanner) DoScan(rootDir string) {
fmt.Printf("=== 开始扫描文件 \n")
if err := filepath.Walk(rootDir, visit); err != nil {
log.Fatal(err)
}
doReadFileMimeInfo()
}
func doReadFileMimeInfo() {
total := len(vars.GlobalFilePathList)
fmt.Printf("=== 文件总数: %d \n", total)
// 扫描文件mime信息
var count = 0
bar := goPrint.NewBar(100)
bar.SetNotice("=== 扫描文件Mime:")
bar.SetGraph(">")
bar.SetNoticeColor(goPrint.FontColor.Red)
for _, currentPath := range vars.GlobalFilePathList {
// mime
mt := util.ReadFileMimeInfo(currentPath)
if mt != nil {
vars.GlobalFilePath2MimeInfoMap[currentPath] = mt.String()
} else {
vars.GlobalFilePath2MimeInfoMap[currentPath] = ""
}
count = count + 1
bar.PrintBar(util.CalcPercentage(count, total))
}
bar.PrintEnd("=== Finish")
}
func (FileScanner) DoFilter() {
total := len(vars.GlobalFilePathList)
var count = 0
bar := goPrint.NewBar(100)
bar.SetNotice("=== 过滤已支持的媒体:")
bar.SetGraph(">")
for _, globalFilePath := range vars.GlobalFilePathList {
fileMime := vars.GlobalFilePath2MimeInfoMap[globalFilePath]
count = count + 1
// 图片
ext := path.Ext(globalFilePath)
if strings.Contains(fileMime, "image/") || util.IsSupportImage(ext) || strings.EqualFold(ext, ".psd") {
vars.GlobalImagePathList = append(vars.GlobalImagePathList, globalFilePath)
bar.PrintBar(util.CalcPercentage(count, total))
continue
}
// 其余作为媒体候选,由ffprobe最终判定(视频/音频/不支持),
// 只要ffmpeg支持的格式都会被识别,不再依赖扩展名白名单
if util.IsPotentialMedia(fileMime) {
vars.GlobalMediaPathList = append(vars.GlobalMediaPathList, globalFilePath)
bar.PrintBar(util.CalcPercentage(count, total))
continue
}
// 明显的非媒体文件(代码/文档/压缩包等)直接不处理
}
bar.PrintEnd("=== Finish")
}
// 定义walkFn回调函数visit
func visit(currentPath string, info os.FileInfo, err error) error {
if err != nil {
return err // 如果有错误,直接返回
}
if info.IsDir() && vars.GlobalPickerDir != "" && currentPath == vars.GlobalPickerDir {
return filepath.SkipDir // 跳过白名单目录(已整理文件的输出目录)
}
if !info.IsDir() {
if filepath.Base(currentPath) == store.DBFileName {
return nil // 跳过媒体信息数据库文件
}
if filepath.Base(currentPath) == util.FFprobeExeName || filepath.Base(currentPath) == "ffprobe_mac" {
return nil // 跳过ffprobe相关文件(中断残留的释放文件 / vars/bin内嵌源文件)
}
vars.GlobalFilePathList = append(vars.GlobalFilePathList, currentPath)
// filename, include ext
vars.GlobalFilePath2FileNameMap[currentPath] = filepath.Base(currentPath)
// file ext
vars.GlobalFilePath2FileExtMap[currentPath] = path.Ext(currentPath)
}
return nil
}
-750
View File
@@ -1,750 +0,0 @@
package core
import (
"fmt"
_ "image/gif" // 导入gif支持
_ "image/jpeg" // 导入jpeg支持
_ "image/png" // 导入png支持
"kenaito-media-picker/util"
"kenaito-media-picker/vars"
"os"
"strings"
)
func DoHandleImage(pickerDir string) {
total := len(vars.GlobalImagePathList) // 总数
successCount := 0 // 成功数
errorCount := 0 // 失败数
ignoreCount := 0 // 忽略数
for _, imageFilePath := range vars.GlobalImagePathList {
suffix := vars.GlobalFilePath2FileExtMap[imageFilePath]
if strings.EqualFold(suffix, ".webp") { // 特殊文件处理, webp为网络常用图片格式
webpErr, webpWidth, webpHeight := ReadWebpImage(imageFilePath)
if webpErr == nil {
vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", webpWidth, webpHeight)
saveMediaRecord(imageFilePath, "image", webpWidth, webpHeight, 0, "", "", "")
successCount = successCount + 1
} else {
errorCount = errorCount + 1
vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath)
fmt.Printf("=== 异常图片: %s \n", imageFilePath)
}
continue
}
if strings.EqualFold(suffix, ".bmp") { // 特殊文件处理
bpmErr, bmpWidth, bmpHeight := ReadBmpImage(imageFilePath)
if bpmErr == nil {
vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", bmpWidth, bmpHeight)
saveMediaRecord(imageFilePath, "image", int(bmpWidth), int(bmpHeight), 0, "", "", "")
successCount = successCount + 1
} else {
errorCount = errorCount + 1
vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath)
fmt.Printf("=== 异常图片: %s \n", imageFilePath)
}
continue
}
if strings.EqualFold(suffix, ".psd") { // 特殊文件处理
vars.ImagePsdList = append(vars.ImagePsdList, imageFilePath)
successCount = successCount + 1
continue
}
if strings.EqualFold(suffix, ".psb") { // 特殊文件处理, psb与psd共享文件头
psbErr, psbWidth, psbHeight := ReadPsdImage(imageFilePath)
if psbErr == nil {
vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", psbWidth, psbHeight)
saveMediaRecord(imageFilePath, "image", psbWidth, psbHeight, 0, "", "", "")
successCount = successCount + 1
} else {
errorCount = errorCount + 1
vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath)
fmt.Printf("=== 异常图片: %s \n", imageFilePath)
}
continue
}
if util.IsSupportImage(suffix) {
err, width, height := util.ReadImageInfo(imageFilePath)
if err == nil {
successCount = successCount + 1
vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", width, height)
saveMediaRecord(imageFilePath, "image", width, height, 0, "", "", "")
fmt.Printf("=== Image总数: %d, 已读取Info: %d, 成功数: %d, 失败数: %d \n", total, successCount+errorCount+ignoreCount, successCount, errorCount)
} else if util.CheckFileIsExist(util.FFprobeExePath()) {
// Go标准库解码失败,尝试用ffprobe兜底(ffmpeg支持的图片格式更广,如tiff/svg等)
if probe, perr := util.ProbeMediaFile(util.FFprobeExePath(), imageFilePath); perr == nil {
if w, h := probe.VideoResolution(); w > 0 && h > 0 {
successCount = successCount + 1
vars.ImagePath2WidthHeightMap[imageFilePath] = fmt.Sprintf("%d-%d", w, h)
saveMediaRecord(imageFilePath, "image", w, h, 0, probe.VideoCodec(), probe.FormatName(), probe.RawJSON())
continue
}
}
errorCount = errorCount + 1
vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath)
fmt.Printf("=== 异常图片: %s \n", imageFilePath)
} else {
errorCount = errorCount + 1
vars.ImageReadErrorPathList = append(vars.ImageReadErrorPathList, imageFilePath)
fmt.Printf("=== 异常图片: %s \n", imageFilePath)
}
continue
}
// 其他的直接先忽略吧, 爱改改, 不改拉倒
ignoreCount = ignoreCount + 1
vars.ImageIgnorePathList = append(vars.ImageIgnorePathList, imageFilePath)
}
if len(vars.ImagePsdList) > 0 {
psdImagePath := pickerDir + string(os.PathSeparator) + "图片_PSD"
if util.CreateDir(psdImagePath) {
util.DoMoveFileToDir(vars.ImagePsdList, psdImagePath)
}
}
if len(vars.ImageReadErrorPathList) > 0 {
readInfoErrorPath := pickerDir + string(os.PathSeparator) + "图片_读取异常"
if util.CreateDir(readInfoErrorPath) {
util.DoMoveFileToDir(vars.ImageReadErrorPathList, readInfoErrorPath)
}
}
if len(vars.ImageIgnorePathList) > 0 {
ignorePath := pickerDir + string(os.PathSeparator) + "图片_已忽略"
if util.CreateDir(ignorePath) {
util.DoMoveFileToDir(vars.ImageIgnorePathList, ignorePath)
}
}
doPickImageFile(pickerDir, vars.ImagePath2WidthHeightMap)
fmt.Printf("=== 图片处理完毕 \n\n")
}
// 条件图片并分组存放
func doPickImageFile(pickerDir string, imagePath2WidthHeightMap map[string]string) {
if len(imagePath2WidthHeightMap) == 0 {
fmt.Printf("=== 当前目录下没有扫描到图片文件, %s \n", pickerDir)
return
}
for currentImagePath, infoStr := range imagePath2WidthHeightMap {
width2Height := strings.Split(infoStr, "-")
width := util.String2int(width2Height[0])
height := util.String2int(width2Height[1])
suffix := vars.GlobalFilePath2FileExtMap[currentImagePath]
if strings.EqualFold(suffix, ".gif") {
handleGifImage(currentImagePath)
continue
}
if width > height {
handleHorizontalImage(currentImagePath, width, height)
continue
}
if width < height {
handleVerticalImage(currentImagePath, height)
continue
}
handleSquareImage(currentImagePath, width)
}
moveHorizontalImage(pickerDir)
moveVerticalImage(pickerDir)
moveSquareImage(pickerDir)
moveGifImage(pickerDir)
}
// 统一处理gif
func handleGifImage(path string) {
vars.ImageGifList = append(vars.ImageGifList, path)
}
// 处理垂直图片
func handleVerticalImage(currentImagePath string, height int) {
// 非标规格
if height < 1024 {
vars.ImageVerticalNormalList = append(vars.ImageVerticalNormalList, currentImagePath)
} else if height < 2048 {
vars.ImageVertical1KList = append(vars.ImageVertical1KList, currentImagePath)
} else if height < 3072 {
vars.ImageVertical2KList = append(vars.ImageVertical2KList, currentImagePath)
} else if height < 4096 {
vars.ImageVertical3KList = append(vars.ImageVertical3KList, currentImagePath)
} else if height < 5120 {
vars.ImageVertical4KList = append(vars.ImageVertical4KList, currentImagePath)
} else if height < 6144 {
vars.ImageVertical5KList = append(vars.ImageVertical5KList, currentImagePath)
} else if height < 7168 {
vars.ImageVertical6KList = append(vars.ImageVertical6KList, currentImagePath)
} else if height < 8192 {
vars.ImageVertical7KList = append(vars.ImageVertical7KList, currentImagePath)
} else if height < 9216 {
vars.ImageVertical8KList = append(vars.ImageVertical8KList, currentImagePath)
} else if height < 10240 {
vars.ImageVertical9KList = append(vars.ImageVertical9KList, currentImagePath)
} else if height < 11264 {
vars.ImageVertical10KList = append(vars.ImageVertical10KList, currentImagePath)
} else if height < 12288 {
vars.ImageVertical11KList = append(vars.ImageVertical11KList, currentImagePath)
} else if height < 13312 {
vars.ImageVertical12KList = append(vars.ImageVertical12KList, currentImagePath)
} else if height < 14336 {
vars.ImageVertical13KList = append(vars.ImageVertical13KList, currentImagePath)
} else if height < 15360 {
vars.ImageVertical14KList = append(vars.ImageVertical14KList, currentImagePath)
} else if height < 16384 {
vars.ImageVertical15KList = append(vars.ImageVertical15KList, currentImagePath)
} else if height < 17408 {
vars.ImageVertical16KList = append(vars.ImageVertical16KList, currentImagePath)
} else if height < 18432 {
vars.ImageVertical17KList = append(vars.ImageVertical17KList, currentImagePath)
} else if height < 19456 {
vars.ImageVertical18KList = append(vars.ImageVertical18KList, currentImagePath)
} else if height < 20480 {
vars.ImageVertical19KList = append(vars.ImageVertical19KList, currentImagePath)
} else if height >= 20480 { // 提示请忽略
vars.ImageVerticalHKList = append(vars.ImageVerticalHKList, currentImagePath)
}
}
// 处理横向图片
func handleHorizontalImage(currentImagePath string, width int, height int) {
// 1280×720(720p)—— 标准高清(HD),常用于早期高清视频或小型屏幕
if width == 1280 && height == 720 {
vars.ImageHorizontalStandard720PList = append(vars.ImageHorizontalStandard720PList, currentImagePath)
return
}
// 1600×900—— 介于HD和FHD之间
if width == 1600 && height == 900 {
vars.ImageHorizontalStandard1D5KList = append(vars.ImageHorizontalStandard1D5KList, currentImagePath)
return
}
// 1920×1080(1080p)—— 全高清,主流显示器、电视和视频的分辨率
if width == 1920 && height == 1080 {
vars.ImageHorizontalStandard1080PList = append(vars.ImageHorizontalStandard1080PList, currentImagePath)
return
}
// 2048×1080(2dk)—— 影院2k
if width == 2048 && height == 1080 {
vars.ImageHorizontalStandard2DKList = append(vars.ImageHorizontalStandard2DKList, currentImagePath)
return
}
// 2560×1440(1440p)—— 俗称“2.5K”,电竞显示器或高端手机屏幕
if width == 2560 && height == 1440 {
vars.ImageHorizontalStandard2D5KList = append(vars.ImageHorizontalStandard2D5KList, currentImagePath)
return
}
// 2560×1080—— 带鱼屏显示器(超宽屏)
if width == 2560 && height == 1080 {
vars.ImageHorizontalStandardUltraWide2List = append(vars.ImageHorizontalStandardUltraWide2List, currentImagePath)
return
}
// 3440×1440—— 带鱼屏显示器(超宽屏)
if width == 3440 && height == 1440 {
vars.ImageHorizontalStandardUltraWide3List = append(vars.ImageHorizontalStandardUltraWide3List, currentImagePath)
return
}
// 5120×2160—— 带鱼屏显示器(超宽屏)
if width == 5120 && height == 2160 {
vars.ImageHorizontalStandardUltraWide4List = append(vars.ImageHorizontalStandardUltraWide4List, currentImagePath)
return
}
// 3840×2160(主流4K)—— 超高清,现代高端显示器、电视和影视制作
if width == 3840 && height == 2160 {
vars.ImageHorizontalStandard4KList = append(vars.ImageHorizontalStandard4KList, currentImagePath)
return
}
// 4096×2160(DCI 4K)—— 电影行业标准
if width == 4096 && height == 2160 {
vars.ImageHorizontalStandard4KHList = append(vars.ImageHorizontalStandard4KHList, currentImagePath)
return
}
// 7680×4320—— 超高清,用于专业影视或高端设备
if width == 7680 && height == 4320 {
vars.ImageHorizontalStandard8KList = append(vars.ImageHorizontalStandard8KList, currentImagePath)
return
}
// 非标规格
if width < 1024 {
vars.ImageHorizontalNormalList = append(vars.ImageHorizontalNormalList, currentImagePath)
} else if width < 2048 {
vars.ImageHorizontal1KList = append(vars.ImageHorizontal1KList, currentImagePath)
} else if width < 3072 {
vars.ImageHorizontal2KList = append(vars.ImageHorizontal2KList, currentImagePath)
} else if width < 4096 {
vars.ImageHorizontal3KList = append(vars.ImageHorizontal3KList, currentImagePath)
} else if width < 5120 {
vars.ImageHorizontal4KList = append(vars.ImageHorizontal4KList, currentImagePath)
} else if width < 6144 {
vars.ImageHorizontal5KList = append(vars.ImageHorizontal5KList, currentImagePath)
} else if width < 7168 {
vars.ImageHorizontal6KList = append(vars.ImageHorizontal6KList, currentImagePath)
} else if width < 8192 {
vars.ImageHorizontal7KList = append(vars.ImageHorizontal7KList, currentImagePath)
} else if width < 9216 {
vars.ImageHorizontal8KList = append(vars.ImageHorizontal8KList, currentImagePath)
} else if width < 10240 {
vars.ImageHorizontal9KList = append(vars.ImageHorizontal9KList, currentImagePath)
} else if width < 11264 {
vars.ImageHorizontal10KList = append(vars.ImageHorizontal10KList, currentImagePath)
} else if width < 12288 {
vars.ImageHorizontal11KList = append(vars.ImageHorizontal11KList, currentImagePath)
} else if width < 13312 {
vars.ImageHorizontal12KList = append(vars.ImageHorizontal12KList, currentImagePath)
} else if width < 14336 {
vars.ImageHorizontal13KList = append(vars.ImageHorizontal13KList, currentImagePath)
} else if width < 15360 {
vars.ImageHorizontal14KList = append(vars.ImageHorizontal14KList, currentImagePath)
} else if width < 16384 {
vars.ImageHorizontal15KList = append(vars.ImageHorizontal15KList, currentImagePath)
} else if width < 17408 {
vars.ImageHorizontal16KList = append(vars.ImageHorizontal16KList, currentImagePath)
} else if width < 18432 {
vars.ImageHorizontal17KList = append(vars.ImageHorizontal17KList, currentImagePath)
} else if width < 19456 {
vars.ImageHorizontal18KList = append(vars.ImageHorizontal18KList, currentImagePath)
} else if width < 20480 {
vars.ImageHorizontal19KList = append(vars.ImageHorizontal19KList, currentImagePath)
} else if width >= 20480 { // 提示请忽略
vars.ImageHorizontalHKList = append(vars.ImageHorizontalHKList, currentImagePath)
}
}
// 处理等比图片
func handleSquareImage(currentImagePath string, width int) {
// 非标规格
if width < 1024 {
vars.ImageSquareNormalList = append(vars.ImageSquareNormalList, currentImagePath)
} else if width < 2048 {
vars.ImageSquare1KList = append(vars.ImageSquare1KList, currentImagePath)
} else if width < 3072 {
vars.ImageSquare2KList = append(vars.ImageSquare2KList, currentImagePath)
} else if width < 4096 {
vars.ImageSquare3KList = append(vars.ImageSquare3KList, currentImagePath)
} else if width < 5120 {
vars.ImageSquare4KList = append(vars.ImageSquare4KList, currentImagePath)
} else if width < 6144 {
vars.ImageSquare5KList = append(vars.ImageSquare5KList, currentImagePath)
} else if width < 7168 {
vars.ImageSquare6KList = append(vars.ImageSquare6KList, currentImagePath)
} else if width < 8192 {
vars.ImageSquare7KList = append(vars.ImageSquare7KList, currentImagePath)
} else if width < 9216 {
vars.ImageSquare8KList = append(vars.ImageSquare8KList, currentImagePath)
} else if width < 10240 {
vars.ImageSquare9KList = append(vars.ImageSquare9KList, currentImagePath)
} else if width < 11264 {
vars.ImageSquare10KList = append(vars.ImageSquare10KList, currentImagePath)
} else if width < 12288 {
vars.ImageSquare11KList = append(vars.ImageSquare11KList, currentImagePath)
} else if width < 13312 {
vars.ImageSquare12KList = append(vars.ImageSquare12KList, currentImagePath)
} else if width < 14336 {
vars.ImageSquare13KList = append(vars.ImageSquare13KList, currentImagePath)
} else if width < 15360 {
vars.ImageSquare14KList = append(vars.ImageSquare14KList, currentImagePath)
} else if width < 16384 {
vars.ImageSquare15KList = append(vars.ImageSquare15KList, currentImagePath)
} else if width < 17408 {
vars.ImageSquare16KList = append(vars.ImageSquare16KList, currentImagePath)
} else if width < 18432 {
vars.ImageSquare17KList = append(vars.ImageSquare17KList, currentImagePath)
} else if width < 19456 {
vars.ImageSquare18KList = append(vars.ImageSquare18KList, currentImagePath)
} else if width < 20480 {
vars.ImageSquare19KList = append(vars.ImageSquare19KList, currentImagePath)
} else if width >= 20480 {
vars.ImageSquareHKList = append(vars.ImageSquareHKList, currentImagePath)
}
}
// 移动水平图片
func moveHorizontalImage(pickerDir string) {
pathSeparator := string(os.PathSeparator)
// 标准
if len(vars.ImageHorizontalStandard720PList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_720P"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard720PList, imageDirPath)
}
if len(vars.ImageHorizontalStandard1D5KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_1D5KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard1D5KList, imageDirPath)
}
if len(vars.ImageHorizontalStandard1080PList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_1080P"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard1080PList, imageDirPath)
}
if len(vars.ImageHorizontalStandard2DKList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_2DKP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard2DKList, imageDirPath)
}
if len(vars.ImageHorizontalStandard2D5KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_2D5KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard2D5KList, imageDirPath)
}
if len(vars.ImageHorizontalStandardUltraWide2List) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_ultraWide2KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandardUltraWide2List, imageDirPath)
}
if len(vars.ImageHorizontalStandardUltraWide3List) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_ultraWide3KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandardUltraWide3List, imageDirPath)
}
if len(vars.ImageHorizontalStandardUltraWide4List) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_ultraWide4KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandardUltraWide4List, imageDirPath)
}
if len(vars.ImageHorizontalStandard4KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_4KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard4KList, imageDirPath)
}
if len(vars.ImageHorizontalStandard4KHList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_4KHP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard4KHList, imageDirPath)
}
if len(vars.ImageHorizontalStandard8KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_8KP"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalStandard8KList, imageDirPath)
}
// 非标准
if len(vars.ImageHorizontalNormalList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_普通"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalNormalList, imageDirPath)
}
if len(vars.ImageHorizontal1KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_1K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal1KList, imageDirPath)
}
if len(vars.ImageHorizontal2KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_2K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal2KList, imageDirPath)
}
if len(vars.ImageHorizontal3KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_3K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal3KList, imageDirPath)
}
if len(vars.ImageHorizontal4KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_4K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal4KList, imageDirPath)
}
if len(vars.ImageHorizontal5KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_5K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal5KList, imageDirPath)
}
if len(vars.ImageHorizontal6KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_6K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal6KList, imageDirPath)
}
if len(vars.ImageHorizontal7KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_7K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal7KList, imageDirPath)
}
if len(vars.ImageHorizontal8KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_8K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal8KList, imageDirPath)
}
if len(vars.ImageHorizontal9KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_9K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal9KList, imageDirPath)
}
if len(vars.ImageHorizontal10KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_10K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal10KList, imageDirPath)
}
if len(vars.ImageHorizontal11KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_11K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal11KList, imageDirPath)
}
if len(vars.ImageHorizontal12KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_12K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal12KList, imageDirPath)
}
if len(vars.ImageHorizontal13KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_13K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal13KList, imageDirPath)
}
if len(vars.ImageHorizontal14KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_14K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal14KList, imageDirPath)
}
if len(vars.ImageHorizontal15KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_15K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal15KList, imageDirPath)
}
if len(vars.ImageHorizontal16KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_16K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal16KList, imageDirPath)
}
if len(vars.ImageHorizontal17KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_17K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal17KList, imageDirPath)
}
if len(vars.ImageHorizontal18KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_18K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal18KList, imageDirPath)
}
if len(vars.ImageHorizontal19KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_19K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontal19KList, imageDirPath)
}
if len(vars.ImageHorizontalHKList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_横屏_原图"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageHorizontalHKList, imageDirPath)
}
}
// 移动垂直图片
func moveVerticalImage(pickerDir string) {
pathSeparator := string(os.PathSeparator)
// 非标准
if len(vars.ImageVerticalNormalList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_普通"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVerticalNormalList, imageDirPath)
}
if len(vars.ImageVertical1KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_1K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical1KList, imageDirPath)
}
if len(vars.ImageVertical2KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_2K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical2KList, imageDirPath)
}
if len(vars.ImageVertical3KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_3K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical3KList, imageDirPath)
}
if len(vars.ImageVertical4KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_4K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical4KList, imageDirPath)
}
if len(vars.ImageVertical5KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_5K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical5KList, imageDirPath)
}
if len(vars.ImageVertical6KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_6K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical6KList, imageDirPath)
}
if len(vars.ImageVertical7KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_7K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical7KList, imageDirPath)
}
if len(vars.ImageVertical8KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_8K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical8KList, imageDirPath)
}
if len(vars.ImageVertical9KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_9K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical9KList, imageDirPath)
}
if len(vars.ImageVertical10KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_10K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical10KList, imageDirPath)
}
if len(vars.ImageVertical11KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_11K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical11KList, imageDirPath)
}
if len(vars.ImageVertical12KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_12K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical12KList, imageDirPath)
}
if len(vars.ImageVertical13KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_13K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical13KList, imageDirPath)
}
if len(vars.ImageVertical14KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_14K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical14KList, imageDirPath)
}
if len(vars.ImageVertical15KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_15K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical15KList, imageDirPath)
}
if len(vars.ImageVertical16KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_16K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical16KList, imageDirPath)
}
if len(vars.ImageVertical17KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_17K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical17KList, imageDirPath)
}
if len(vars.ImageVertical18KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_18K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical18KList, imageDirPath)
}
if len(vars.ImageVertical19KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_19K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVertical19KList, imageDirPath)
}
if len(vars.ImageVerticalHKList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_竖屏_原图"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageVerticalHKList, imageDirPath)
}
}
// 移动等比图片
func moveSquareImage(pickerDir string) {
pathSeparator := string(os.PathSeparator)
// 非标准
if len(vars.ImageSquareNormalList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_普通"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquareNormalList, imageDirPath)
}
if len(vars.ImageSquare1KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_1K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare1KList, imageDirPath)
}
if len(vars.ImageSquare2KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_2K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare2KList, imageDirPath)
}
if len(vars.ImageSquare3KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_3K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare3KList, imageDirPath)
}
if len(vars.ImageSquare4KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_4K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare4KList, imageDirPath)
}
if len(vars.ImageSquare5KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_5K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare5KList, imageDirPath)
}
if len(vars.ImageSquare6KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_6K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare6KList, imageDirPath)
}
if len(vars.ImageSquare7KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_7K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare7KList, imageDirPath)
}
if len(vars.ImageSquare8KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_8K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare8KList, imageDirPath)
}
if len(vars.ImageSquare9KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_9K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare9KList, imageDirPath)
}
if len(vars.ImageSquare10KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_10K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare10KList, imageDirPath)
}
if len(vars.ImageSquare11KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_11K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare11KList, imageDirPath)
}
if len(vars.ImageSquare12KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_12K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare12KList, imageDirPath)
}
if len(vars.ImageSquare13KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_13K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare13KList, imageDirPath)
}
if len(vars.ImageSquare14KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_14K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare14KList, imageDirPath)
}
if len(vars.ImageSquare15KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_15K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare15KList, imageDirPath)
}
if len(vars.ImageSquare16KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_16K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare16KList, imageDirPath)
}
if len(vars.ImageSquare17KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_17K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare17KList, imageDirPath)
}
if len(vars.ImageSquare18KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_18K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare18KList, imageDirPath)
}
if len(vars.ImageSquare19KList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_19K"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquare19KList, imageDirPath)
}
if len(vars.ImageSquareHKList) > 0 {
imageDirPath := pickerDir + pathSeparator + "图片_等比_原图"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageSquareHKList, imageDirPath)
}
}
// 移动gif图片
func moveGifImage(pickerDir string) {
if len(vars.ImageGifList) > 0 {
pathSeparator := string(os.PathSeparator)
imageDirPath := pickerDir + pathSeparator + "图片_GIF"
util.CreateDir(imageDirPath)
util.DoMoveFileToDir(vars.ImageGifList, imageDirPath)
}
}
-718
View File
@@ -1,718 +0,0 @@
package core
import (
"fmt"
"kenaito-media-picker/util"
"kenaito-media-picker/vars"
"os"
"strings"
)
// DoHandleMedia 处理所有媒体候选文件:ffprobe判定视频/音频/不支持
func DoHandleMedia(pickerDir string) {
total := len(vars.GlobalMediaPathList)
videoCount := 0 // 视频数
audioCount := 0 // 音频数
errorCount := 0 // 异常数
ignoreCount := 0
for _, mediaFilePath := range vars.GlobalMediaPathList {
fileMime := vars.GlobalFilePath2MimeInfoMap[mediaFilePath]
probe, err := util.ProbeMediaFile(util.FFprobeExePath(), mediaFilePath)
if err != nil {
if strings.Contains(fileMime, "video/") {
// mime是视频但ffprobe读不出来,视为损坏/异常视频
errorCount = errorCount + 1
vars.VideoReadErrorPathList = append(vars.VideoReadErrorPathList, mediaFilePath)
fmt.Printf("=== 异常视频: %s \n", mediaFilePath)
} else {
ignoreCount = ignoreCount + 1
vars.VideoIgnorePathList = append(vars.VideoIgnorePathList, mediaFilePath)
}
continue
}
width, height := probe.VideoResolution()
if width > 0 && height > 0 { // 有视频流
videoCount = videoCount + 1
vars.VideoPath2WidthHeightMap[mediaFilePath] = fmt.Sprintf("%d-%d", width, height)
saveMediaRecord(mediaFilePath, "video", width, height, probe.DurationSeconds(), probe.VideoCodec(), probe.FormatName(), probe.RawJSON())
fmt.Printf("=== 媒体总数: %d, 已处理: %d, 视频: %d, 音频: %d, 异常: %d, 忽略: %d \n", total, videoCount+audioCount+errorCount+ignoreCount, videoCount, audioCount, errorCount, ignoreCount)
continue
}
if probe.HasAudio() { // 仅有音频流
audioCount = audioCount + 1
vars.GlobalAudioPathList = append(vars.GlobalAudioPathList, mediaFilePath)
saveMediaRecord(mediaFilePath, "audio", 0, 0, probe.DurationSeconds(), probe.AudioCodec(), probe.FormatName(), probe.RawJSON())
continue
}
// 既无视频也无音频(字幕流等)
ignoreCount = ignoreCount + 1
vars.VideoIgnorePathList = append(vars.VideoIgnorePathList, mediaFilePath)
}
if len(vars.VideoReadErrorPathList) > 0 {
readInfoErrorPath := pickerDir + string(os.PathSeparator) + "视频_读取异常"
if util.CreateDir(readInfoErrorPath) {
util.DoMoveFileToDir(vars.VideoReadErrorPathList, readInfoErrorPath)
}
}
if len(vars.GlobalAudioPathList) > 0 {
audioPath := pickerDir + string(os.PathSeparator) + "音频"
if util.CreateDir(audioPath) {
util.DoMoveFileToDir(vars.GlobalAudioPathList, audioPath)
}
}
if len(vars.VideoIgnorePathList) > 0 {
ignorePath := pickerDir + string(os.PathSeparator) + "已忽略"
if util.CreateDir(ignorePath) {
util.DoMoveFileToDir(vars.VideoIgnorePathList, ignorePath)
}
}
doPickVideoFile(pickerDir, vars.VideoPath2WidthHeightMap)
fmt.Printf("=== 视频/音频处理完毕 \n\n")
}
// 条件视频并分组存放
func doPickVideoFile(pickerDir string, videoPath2WidthHeightMap map[string]string) {
if len(videoPath2WidthHeightMap) == 0 {
fmt.Printf("=== 当前目录下没有扫描到视频文件, %s \n", pickerDir)
return
}
for currentVideoPath, infoStr := range videoPath2WidthHeightMap {
width2Height := strings.Split(infoStr, "-")
width := util.String2int(width2Height[0])
height := util.String2int(width2Height[1])
suffix := vars.GlobalFilePath2FileExtMap[currentVideoPath]
if width > height {
handleHorizontalVideo(currentVideoPath, width, height, suffix)
continue
}
if width < height {
handleVerticalVideo(currentVideoPath, height, suffix)
continue
}
handleSquareVideo(currentVideoPath, width, height, suffix)
}
moveNormalVideo(pickerDir)
moveHorizontalVideo(pickerDir)
moveVerticalVideo(pickerDir)
moveSquareVideo(pickerDir)
moveGifVideo(pickerDir)
}
// 移动垂直视频
func moveVerticalVideo(pickerDir string) {
pathSeparator := string(os.PathSeparator)
if len(vars.VideoVertical1KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_1k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical1KList, videoDirPath)
}
if len(vars.VideoVertical2KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_2k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical2KList, videoDirPath)
}
if len(vars.VideoVertical3KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_3k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical3KList, videoDirPath)
}
if len(vars.VideoVertical4KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_4k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical4KList, videoDirPath)
}
if len(vars.VideoVertical5KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_5k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical5KList, videoDirPath)
}
if len(vars.VideoVertical6KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_6k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical6KList, videoDirPath)
}
if len(vars.VideoVertical7KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_7k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical7KList, videoDirPath)
}
if len(vars.VideoVertical8KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_8k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical8KList, videoDirPath)
}
if len(vars.VideoVertical9KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_9k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical9KList, videoDirPath)
}
if len(vars.VideoVertical10KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_10k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical10KList, videoDirPath)
}
if len(vars.VideoVertical11KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_11k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical11KList, videoDirPath)
}
if len(vars.VideoVertical12KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_12k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical12KList, videoDirPath)
}
if len(vars.VideoVertical13KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_13k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical13KList, videoDirPath)
}
if len(vars.VideoVertical14KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_14k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical14KList, videoDirPath)
}
if len(vars.VideoVertical15KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_15k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical15KList, videoDirPath)
}
if len(vars.VideoVertical16KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_16k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical16KList, videoDirPath)
}
if len(vars.VideoVertical17KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_17k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical17KList, videoDirPath)
}
if len(vars.VideoVertical18KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_18k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical18KList, videoDirPath)
}
if len(vars.VideoVertical19KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_19k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVertical19KList, videoDirPath)
}
if len(vars.VideoVerticalHKList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_原画质"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVerticalHKList, videoDirPath)
}
}
// 移动水平视频
func moveHorizontalVideo(pickerDir string) {
pathSeparator := string(os.PathSeparator)
if len(vars.VideoHorizontal1KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_1k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal1KList, videoDirPath)
}
if len(vars.VideoHorizontal2KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_2k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal2KList, videoDirPath)
}
if len(vars.VideoHorizontal3KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_3k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal3KList, videoDirPath)
}
if len(vars.VideoHorizontal4KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_4k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal4KList, videoDirPath)
}
if len(vars.VideoHorizontal5KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_5k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal5KList, videoDirPath)
}
if len(vars.VideoHorizontal6KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_6k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal6KList, videoDirPath)
}
if len(vars.VideoHorizontal7KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_7k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal7KList, videoDirPath)
}
if len(vars.VideoHorizontal8KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_8k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal8KList, videoDirPath)
}
if len(vars.VideoHorizontal9KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_9k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal9KList, videoDirPath)
}
if len(vars.VideoHorizontal10KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_10k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal10KList, videoDirPath)
}
if len(vars.VideoHorizontal11KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_11k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal11KList, videoDirPath)
}
if len(vars.VideoHorizontal12KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_12k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal12KList, videoDirPath)
}
if len(vars.VideoHorizontal13KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_13k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal13KList, videoDirPath)
}
if len(vars.VideoHorizontal14KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_14k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal14KList, videoDirPath)
}
if len(vars.VideoHorizontal15KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_15k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal15KList, videoDirPath)
}
if len(vars.VideoHorizontal16KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_16k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal16KList, videoDirPath)
}
if len(vars.VideoHorizontal17KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_17k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal17KList, videoDirPath)
}
if len(vars.VideoHorizontal18KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_18k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal18KList, videoDirPath)
}
if len(vars.VideoHorizontal19KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_19k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontal19KList, videoDirPath)
}
if len(vars.VideoHorizontalHKList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_原画质"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalHKList, videoDirPath)
}
if len(vars.VideoHorizontalStandard720PList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_720P"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard720PList, videoDirPath)
}
if len(vars.VideoHorizontalStandard1D5PList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_1D5KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard1D5PList, videoDirPath)
}
if len(vars.VideoHorizontalStandard1080PList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_1080P"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard1080PList, videoDirPath)
}
if len(vars.VideoHorizontalStandard2DKList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_2DKP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard2DKList, videoDirPath)
}
if len(vars.VideoHorizontalStandard2D5KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_2D5KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard2D5KList, videoDirPath)
}
if len(vars.VideoHorizontalStandardUltraWide2List) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_UltraWide2KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandardUltraWide2List, videoDirPath)
}
if len(vars.VideoHorizontalStandardUltraWide3List) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_UltraWide3KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandardUltraWide3List, videoDirPath)
}
if len(vars.VideoHorizontalStandardUltraWide4List) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_UltraWide4KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandardUltraWide4List, videoDirPath)
}
if len(vars.VideoHorizontalStandard4KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_4KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard4KList, videoDirPath)
}
if len(vars.VideoHorizontalStandard4KHList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_4KHP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard4KHList, videoDirPath)
}
if len(vars.VideoHorizontalStandard8KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_8KP"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalStandard8KList, videoDirPath)
}
}
// 移动等比视频
func moveSquareVideo(pickerDir string) {
pathSeparator := string(os.PathSeparator)
if len(vars.VideoSquare1KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_1k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare1KList, videoDirPath)
}
if len(vars.VideoSquare2KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_2k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare2KList, videoDirPath)
}
if len(vars.VideoSquare3KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_3k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare3KList, videoDirPath)
}
if len(vars.VideoSquare4KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_4k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare4KList, videoDirPath)
}
if len(vars.VideoSquare5KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_5k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare5KList, videoDirPath)
}
if len(vars.VideoSquare6KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_6k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare6KList, videoDirPath)
}
if len(vars.VideoSquare7KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_7k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare7KList, videoDirPath)
}
if len(vars.VideoSquare8KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_8k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare8KList, videoDirPath)
}
if len(vars.VideoSquare9KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_9k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare9KList, videoDirPath)
}
if len(vars.VideoSquare10KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_10k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare10KList, videoDirPath)
}
if len(vars.VideoSquare11KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_11k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare11KList, videoDirPath)
}
if len(vars.VideoSquare12KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_12k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare12KList, videoDirPath)
}
if len(vars.VideoSquare13KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_13k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare13KList, videoDirPath)
}
if len(vars.VideoSquare14KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_14k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare14KList, videoDirPath)
}
if len(vars.VideoSquare15KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_15k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare15KList, videoDirPath)
}
if len(vars.VideoSquare16KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_16k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare16KList, videoDirPath)
}
if len(vars.VideoSquare17KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_17k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare17KList, videoDirPath)
}
if len(vars.VideoSquare18KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_18k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare18KList, videoDirPath)
}
if len(vars.VideoSquare19KList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_19k"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquare19KList, videoDirPath)
}
if len(vars.VideoSquareHKList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_原画质"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquareHKList, videoDirPath)
}
}
// 移动gif视频
func moveGifVideo(pickerDir string) {
pathSeparator := string(os.PathSeparator)
if len(vars.VideoHorizontalGifList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_GIF"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalGifList, videoDirPath)
}
if len(vars.VideoVerticalGifList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_GIF"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVerticalGifList, videoDirPath)
}
if len(vars.VideoSquareGifList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_GIF"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquareGifList, videoDirPath)
}
}
// 移动普通视频
func moveNormalVideo(pickerDir string) {
pathSeparator := string(os.PathSeparator)
if len(vars.VideoHorizontalNormalList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_横屏_普通"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoHorizontalNormalList, videoDirPath)
}
if len(vars.VideoVerticalNormalList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_竖屏_普通"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoVerticalNormalList, videoDirPath)
}
if len(vars.VideoSquareNormalList) > 0 {
videoDirPath := pickerDir + pathSeparator + "视频_等比_普通"
util.CreateDir(videoDirPath)
util.DoMoveFileToDir(vars.VideoSquareNormalList, videoDirPath)
}
}
// 处理垂直视频
func handleVerticalVideo(currentVideoPath string, height int, suffix string) {
if strings.EqualFold(suffix, ".gif") {
vars.VideoVerticalGifList = append(vars.VideoVerticalGifList, currentVideoPath)
return
}
if height < 1024 {
vars.VideoVerticalNormalList = append(vars.VideoVerticalNormalList, currentVideoPath)
} else if height < 2048 {
vars.VideoVertical1KList = append(vars.VideoVertical1KList, currentVideoPath)
} else if height < 3072 {
vars.VideoVertical2KList = append(vars.VideoVertical2KList, currentVideoPath)
} else if height < 4096 {
vars.VideoVertical3KList = append(vars.VideoVertical3KList, currentVideoPath)
} else if height < 5120 {
vars.VideoVertical4KList = append(vars.VideoVertical4KList, currentVideoPath)
} else if height < 6144 {
vars.VideoVertical5KList = append(vars.VideoVertical5KList, currentVideoPath)
} else if height < 7168 {
vars.VideoVertical6KList = append(vars.VideoVertical6KList, currentVideoPath)
} else if height < 8192 {
vars.VideoVertical7KList = append(vars.VideoVertical7KList, currentVideoPath)
} else if height < 9216 {
vars.VideoVertical8KList = append(vars.VideoVertical8KList, currentVideoPath)
} else if height < 10240 {
vars.VideoVertical9KList = append(vars.VideoVertical9KList, currentVideoPath)
} else if height < 11264 {
vars.VideoVertical10KList = append(vars.VideoVertical10KList, currentVideoPath)
} else if height < 12288 {
vars.VideoVertical11KList = append(vars.VideoVertical11KList, currentVideoPath)
} else if height < 13312 {
vars.VideoVertical12KList = append(vars.VideoVertical12KList, currentVideoPath)
} else if height < 14336 {
vars.VideoVertical13KList = append(vars.VideoVertical13KList, currentVideoPath)
} else if height < 15360 {
vars.VideoVertical14KList = append(vars.VideoVertical14KList, currentVideoPath)
} else if height < 16384 {
vars.VideoVertical15KList = append(vars.VideoVertical15KList, currentVideoPath)
} else if height < 17408 {
vars.VideoVertical16KList = append(vars.VideoVertical16KList, currentVideoPath)
} else if height < 18432 {
vars.VideoVertical17KList = append(vars.VideoVertical17KList, currentVideoPath)
} else if height < 19456 {
vars.VideoVertical18KList = append(vars.VideoVertical18KList, currentVideoPath)
} else if height < 20480 {
vars.VideoVertical19KList = append(vars.VideoVertical19KList, currentVideoPath)
} else if height >= 20480 {
vars.VideoVerticalHKList = append(vars.VideoVerticalHKList, currentVideoPath)
}
}
// 处理横向视频
func handleHorizontalVideo(currentVideoPath string, width int, height int, suffix string) {
if strings.EqualFold(suffix, ".gif") {
vars.VideoHorizontalGifList = append(vars.VideoHorizontalGifList, currentVideoPath)
return
}
// 1280×720(720p)—— 标准高清(HD),常用于早期高清视频或小型屏幕
if width == 1280 && height == 720 {
vars.VideoHorizontalStandard720PList = append(vars.VideoHorizontalStandard720PList, currentVideoPath)
return
}
// 1600×900—— 介于HD和FHD之间
if width == 1600 && height == 900 {
vars.VideoHorizontalStandard1D5PList = append(vars.VideoHorizontalStandard1D5PList, currentVideoPath)
return
}
// 1920×1080(1080p)—— 全高清,主流显示器、电视和视频的分辨率
if width == 1920 && height == 1080 {
vars.VideoHorizontalStandard1080PList = append(vars.VideoHorizontalStandard1080PList, currentVideoPath)
return
}
// 2048×1080(2dk)—— 影院2k
if width == 2048 && height == 1080 {
vars.VideoHorizontalStandard2DKList = append(vars.VideoHorizontalStandard2DKList, currentVideoPath)
return
}
// 2560×1440(1440p)—— 俗称“2.5K”,电竞显示器或高端手机屏幕
if width == 2560 && height == 1440 {
vars.VideoHorizontalStandard2D5KList = append(vars.VideoHorizontalStandard2D5KList, currentVideoPath)
return
}
// 2560×1080—— 带鱼屏显示器(超宽屏)
if width == 2560 && height == 1080 {
vars.VideoHorizontalStandardUltraWide2List = append(vars.VideoHorizontalStandardUltraWide2List, currentVideoPath)
return
}
// 3440×1440—— 带鱼屏显示器(超宽屏)
if width == 3440 && height == 1440 {
vars.VideoHorizontalStandardUltraWide3List = append(vars.VideoHorizontalStandardUltraWide3List, currentVideoPath)
return
}
// 5120×2160—— 带鱼屏显示器(超宽屏)
if width == 5120 && height == 2160 {
vars.VideoHorizontalStandardUltraWide4List = append(vars.VideoHorizontalStandardUltraWide4List, currentVideoPath)
return
}
// 3840×2160(主流4K)—— 超高清,现代高端显示器、电视和影视制作
if width == 3840 && height == 2160 {
vars.VideoHorizontalStandard4KList = append(vars.VideoHorizontalStandard4KList, currentVideoPath)
return
}
// 4096×2160(DCI 4K)—— 电影行业标准
if width == 4096 && height == 2160 {
vars.VideoHorizontalStandard4KHList = append(vars.VideoHorizontalStandard4KHList, currentVideoPath)
return
}
// 7680×4320—— 超高清,用于专业影视或高端设备
if width == 7680 && height == 4320 {
vars.VideoHorizontalStandard8KList = append(vars.VideoHorizontalStandard8KList, currentVideoPath)
return
}
// 非标规格
if width < 1024 {
vars.VideoHorizontalNormalList = append(vars.VideoHorizontalNormalList, currentVideoPath)
} else if width < 2048 {
vars.VideoHorizontal1KList = append(vars.VideoHorizontal1KList, currentVideoPath)
} else if width < 3072 {
vars.VideoHorizontal2KList = append(vars.VideoHorizontal2KList, currentVideoPath)
} else if width < 4096 {
vars.VideoHorizontal3KList = append(vars.VideoHorizontal3KList, currentVideoPath)
} else if width < 5120 {
vars.VideoHorizontal4KList = append(vars.VideoHorizontal4KList, currentVideoPath)
} else if width < 6144 {
vars.VideoHorizontal5KList = append(vars.VideoHorizontal5KList, currentVideoPath)
} else if width < 7168 {
vars.VideoHorizontal6KList = append(vars.VideoHorizontal6KList, currentVideoPath)
} else if width < 8192 {
vars.VideoHorizontal7KList = append(vars.VideoHorizontal7KList, currentVideoPath)
} else if width < 9216 {
vars.VideoHorizontal8KList = append(vars.VideoHorizontal8KList, currentVideoPath)
} else if width < 10240 {
vars.VideoHorizontal9KList = append(vars.VideoHorizontal9KList, currentVideoPath)
} else if width < 11264 {
vars.VideoHorizontal10KList = append(vars.VideoHorizontal10KList, currentVideoPath)
} else if width < 12288 {
vars.VideoHorizontal11KList = append(vars.VideoHorizontal11KList, currentVideoPath)
} else if width < 13312 {
vars.VideoHorizontal12KList = append(vars.VideoHorizontal12KList, currentVideoPath)
} else if width < 14336 {
vars.VideoHorizontal13KList = append(vars.VideoHorizontal13KList, currentVideoPath)
} else if width < 15360 {
vars.VideoHorizontal14KList = append(vars.VideoHorizontal14KList, currentVideoPath)
} else if width < 16384 {
vars.VideoHorizontal15KList = append(vars.VideoHorizontal15KList, currentVideoPath)
} else if width < 17408 {
vars.VideoHorizontal16KList = append(vars.VideoHorizontal16KList, currentVideoPath)
} else if width < 18432 {
vars.VideoHorizontal17KList = append(vars.VideoHorizontal17KList, currentVideoPath)
} else if width < 19456 {
vars.VideoHorizontal18KList = append(vars.VideoHorizontal18KList, currentVideoPath)
} else if width < 20480 {
vars.VideoHorizontal19KList = append(vars.VideoHorizontal19KList, currentVideoPath)
} else if width >= 20480 { // 提示请忽略
vars.VideoHorizontalHKList = append(vars.VideoHorizontalHKList, currentVideoPath)
}
}
// 处理等比视频
func handleSquareVideo(currentVideoPath string, width int, height int, suffix string) {
if strings.EqualFold(suffix, ".gif") {
vars.VideoSquareGifList = append(vars.VideoSquareGifList, currentVideoPath)
return
}
if width < 1024 {
vars.VideoSquareNormalList = append(vars.VideoSquareNormalList, currentVideoPath)
} else if width < 2048 {
vars.VideoSquare1KList = append(vars.VideoSquare1KList, currentVideoPath)
} else if width < 3072 {
vars.VideoSquare2KList = append(vars.VideoSquare2KList, currentVideoPath)
} else if width < 4096 {
vars.VideoSquare3KList = append(vars.VideoSquare3KList, currentVideoPath)
} else if width < 5120 {
vars.VideoSquare4KList = append(vars.VideoSquare4KList, currentVideoPath)
} else if width < 6144 {
vars.VideoSquare5KList = append(vars.VideoSquare5KList, currentVideoPath)
} else if width < 7168 {
vars.VideoSquare6KList = append(vars.VideoSquare6KList, currentVideoPath)
} else if width < 8192 {
vars.VideoSquare7KList = append(vars.VideoSquare7KList, currentVideoPath)
} else if width < 9216 {
vars.VideoSquare8KList = append(vars.VideoSquare8KList, currentVideoPath)
} else if width < 10240 {
vars.VideoSquare9KList = append(vars.VideoSquare9KList, currentVideoPath)
} else if width < 11264 {
vars.VideoSquare10KList = append(vars.VideoSquare10KList, currentVideoPath)
} else if width < 12288 {
vars.VideoSquare11KList = append(vars.VideoSquare11KList, currentVideoPath)
} else if width < 13312 {
vars.VideoSquare12KList = append(vars.VideoSquare12KList, currentVideoPath)
} else if width < 14336 {
vars.VideoSquare13KList = append(vars.VideoSquare13KList, currentVideoPath)
} else if width < 15360 {
vars.VideoSquare14KList = append(vars.VideoSquare14KList, currentVideoPath)
} else if width < 16384 {
vars.VideoSquare15KList = append(vars.VideoSquare15KList, currentVideoPath)
} else if width < 17408 {
vars.VideoSquare16KList = append(vars.VideoSquare16KList, currentVideoPath)
} else if width < 18432 {
vars.VideoSquare17KList = append(vars.VideoSquare17KList, currentVideoPath)
} else if width < 19456 {
vars.VideoSquare18KList = append(vars.VideoSquare18KList, currentVideoPath)
} else if width < 20480 {
vars.VideoSquare19KList = append(vars.VideoSquare19KList, currentVideoPath)
} else if width >= 20480 {
vars.VideoSquareHKList = append(vars.VideoSquareHKList, currentVideoPath)
}
}
-57
View File
@@ -1,57 +0,0 @@
package core
import (
"fmt"
"kenaito-media-picker/store"
"kenaito-media-picker/util"
"kenaito-media-picker/vars"
"os"
)
// saveMediaRecord 将媒体信息写入数据库;大文件优先用指纹(头尾采样)复用已入库的MD5,避免全量读取
func saveMediaRecord(filePath string, mediaType string, width int, height int, duration float64, codec string, format string, rawInfo string) {
st, err := os.Stat(filePath)
if err != nil {
fmt.Printf("=== 读取文件信息失败: %s, 错误: %v\n", filePath, err)
return
}
size := st.Size()
md5Str := ""
headMd5, tailMd5 := "", ""
if size > util.FingerprintThreshold {
if head, tail, ferr := util.CalcFileFingerprint(filePath, size); ferr == nil {
headMd5, tailMd5 = head, tail
if existing, ok := store.FindMediaByFingerprint(size, head, tail); ok && existing.FileMd5 != "" {
md5Str = existing.FileMd5 // 指纹命中,复用MD5,跳过全量读取
}
}
}
if md5Str == "" {
if md5Str, err = util.CalcFileMd5(filePath); err != nil {
fmt.Printf("=== 计算MD5失败: %s, 错误: %v\n", filePath, err)
return
}
}
info := &store.MediaInfo{
FileMd5: md5Str,
FilePath: filePath,
FileName: vars.GlobalFilePath2FileNameMap[filePath],
FileExt: vars.GlobalFilePath2FileExtMap[filePath],
FileSize: size,
MimeType: vars.GlobalFilePath2MimeInfoMap[filePath],
MediaType: mediaType,
Width: width,
Height: height,
Duration: duration,
Codec: codec,
Format: format,
RawInfo: rawInfo,
HeadMd5: headMd5,
TailMd5: tailMd5,
}
if err := store.SaveMedia(info); err != nil {
fmt.Printf("=== 保存媒体信息失败: %s, 错误: %v\n", filePath, err)
}
}
-25
View File
@@ -1,25 +0,0 @@
package core
import (
"fmt"
"golang.org/x/image/webp"
"os"
)
// ReadWebpImage 读取webp格式的图片信息
func ReadWebpImage(webpFilePath string) (err error, width int, height int) {
// 打开WebP文件
file, err := os.Open(webpFilePath)
if err != nil {
fmt.Printf("=== Failed to open file: %v\n", err)
return err, 0, 0
}
defer file.Close()
// 使用webp.DecodeConfig解码WebP图片配置信息(不加载完整像素数据)
imgConfig, err := webp.DecodeConfig(file)
if err != nil {
fmt.Printf("=== Failed to decode WebP image config: %v\n", err)
return err, 0, 0
}
return nil, imgConfig.Width, imgConfig.Height
}
+72
View File
@@ -0,0 +1,72 @@
// Package classify 提供纯函数的分辨率归类规则,不依赖任何状态。
package classify
import "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
}
}
func (o Orientation) String() string {
switch o {
case OrientationHorizontal:
return "横屏"
case OrientationVertical:
return "竖屏"
default:
return "等比"
}
}
// ImageDir 返回图片归类子目录名,如 "图片_横屏_1080P" / "图片_GIF"
func ImageDir(ext string, width, height int) string {
if strings.EqualFold(ext, ".gif") {
return "图片_GIF"
}
return resolutionDir("图片", ext, width, height)
}
// VideoDir 返回视频归类子目录名,如 "视频_竖屏_4k" / "视频_横屏_GIF"
func VideoDir(ext string, width, height int) string {
return resolutionDir("视频", ext, width, height)
}
func resolutionDir(kind, ext string, width, height int) string {
ori := OrientationOf(width, height).String()
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
}
+52
View File
@@ -0,0 +1,52 @@
package classify
import "fmt"
// standardResolution 标准分辨率规则:宽高精确匹配
type standardResolution struct {
width int
height int
suffix string
}
var standardResolutions = []standardResolution{
{1280, 720, "720P"}, // 720p 标准高清
{1600, 900, "1D5KP"}, // 介于 HD 与 FHD 之间
{1920, 1080, "1080P"}, // 1080p 全高清
{2048, 1080, "2DKP"}, // 影院 2K
{2560, 1440, "2D5KP"}, // 2.5K
{2560, 1080, "UltraWide2KP"}, // 带鱼屏
{3440, 1440, "UltraWide3KP"}, // 带鱼屏
{5120, 2160, "UltraWide4KP"}, // 带鱼屏
{3840, 2160, "4KP"}, // 主流 4K
{4096, 2160, "4KHP"}, // DCI 4K
{7680, 4320, "8KP"}, // 8K
}
// 非标分辨率分档参数
const (
bucketStep = 1024 // 每 1024px 一档
maxLevel = 19 // 1K ~ 19K 共 19 档(普通另算 1 档,合计 20 档)
)
// matchStandard 精确匹配标准分辨率,命中返回后缀,否则返回空串
func matchStandard(width, height int) string {
for _, r := range standardResolutions {
if r.width == width && r.height == height {
return r.suffix
}
}
return ""
}
// levelName 按长边分档:普通(<1024) / 1K~19K(每1024一档) / overflow(≥20480)
func levelName(longEdge int, overflow string) string {
if longEdge < bucketStep {
return "普通"
}
level := longEdge / bucketStep
if level > maxLevel {
return overflow
}
return fmt.Sprintf("%dK", level)
}
+53
View File
@@ -0,0 +1,53 @@
// Package digest 提供文件 MD5 与指纹采样计算,供媒体入库时复用。
package digest
import (
"crypto/md5"
"encoding/hex"
"io"
"os"
)
const (
// FingerprintThreshold 文件大于该字节数才使用指纹复用,避免全量MD5
FingerprintThreshold = 1 << 20
fingerprintHeadSize = 256 << 10 // 文件头采样256KB
fingerprintTailSize = 256 << 10 // 文件尾采样256KB
)
// CalcFileMd5 计算文件MD5(分块读取,适合大文件)
func CalcFileMd5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
h := md5.New()
if _, err := io.Copy(h, file); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// CalcFileFingerprint 计算文件头部与尾部采样MD5(只读取很少字节,用于快速识别相同文件)
func CalcFileFingerprint(filePath string, size int64) (headMd5, tailMd5 string, err error) {
file, err := os.Open(filePath)
if err != nil {
return "", "", err
}
defer file.Close()
h := md5.New()
if _, err := io.CopyN(h, file, fingerprintHeadSize); err != nil {
return "", "", err
}
headMd5 = hex.EncodeToString(h.Sum(nil))
h = md5.New()
if _, err := file.Seek(size-fingerprintTailSize, io.SeekStart); err != nil {
return "", "", err
}
if _, err := io.CopyN(h, file, fingerprintTailSize); err != nil {
return "", "", err
}
tailMd5 = hex.EncodeToString(h.Sum(nil))
return headMd5, tailMd5, nil
}
+88
View File
@@ -0,0 +1,88 @@
// Package mover 负责文件的移动,跨文件系统时自动回退为复制后删除。
package mover
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/redmask-hb/GoSimplePrint/goPrint"
uuid "github.com/satori/go.uuid"
)
// Mover 文件移动器
type Mover struct{}
// MoveToDir 将一批文件移动到目标目录(目标文件名使用 uuid 前缀防止同名覆盖)。
// 返回移动成功的 源路径 → 目标目录 映射,供记录去向使用。
func (m *Mover) MoveToDir(filePaths []string, targetDir string) map[string]string {
moved := make(map[string]string)
if len(filePaths) == 0 {
return moved
}
if err := os.MkdirAll(targetDir, 0755); err != nil {
fmt.Printf("=== 创建目录失败: %s, 错误: %v\n", targetDir, err)
return moved
}
bar := goPrint.NewBar(100)
bar.SetNotice("=== 移动文件到目录:")
bar.SetGraph(">")
total := len(filePaths)
for i, src := range filePaths {
if moveOne(src, targetDir) {
moved[src] = targetDir
}
bar.PrintBar(percent(i+1, total))
}
bar.PrintEnd("=== Finish")
return moved
}
// moveOne 移动单个文件;跨文件系统时 fallback 为 copy+delete
func moveOne(src, targetDir string) bool {
fileName := strings.ReplaceAll(uuid.NewV4().String(), "-", "") + "_" + filepath.Base(src)
target := filepath.Join(targetDir, fileName)
if err := os.Rename(src, target); err == nil {
return true
}
if err := copyFile(src, target); err != nil {
fmt.Printf("=== 复制文件失败, 源: %s, 目标: %s, 错误: %v\n", src, target, err)
return false
}
if err := os.Remove(src); err != nil {
fmt.Printf("=== 删除源文件失败: %s, 错误: %v\n", src, err)
// 复制成功但删除失败,文件已存在于两处,视为移动成功
}
return true
}
// copyFile 逐块复制文件
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Close()
}
// percent 计算百分比(0~100)
func percent(part, total int) int {
if total <= 0 {
return 100
}
return int(float64(part) / float64(total) * 100)
}
+181
View File
@@ -0,0 +1,181 @@
// Package organize 编排整理流程:探测 → 归类 → 收集 bucket → 一次性移动并记录去向。
package organize
import (
"fmt"
"os"
"path/filepath"
"strings"
"kenaito-media-picker/internal/classify"
"kenaito-media-picker/internal/digest"
"kenaito-media-picker/internal/mover"
"kenaito-media-picker/internal/probe"
"kenaito-media-picker/internal/scanner"
"kenaito-media-picker/internal/store"
)
// 特殊归类目录名
const (
dirImagePSD = "图片_PSD"
dirImageErr = "图片_读取异常"
dirImageIgnored = "图片_已忽略"
dirVideoErr = "视频_读取异常"
dirVideoIgnored = "视频_已忽略"
dirAudio = "音频"
)
// Organizer 整理器:按目标目录收集文件,处理完成后统一建目录并移动
type Organizer struct {
pickerDir string
ffprobePath string
store *store.Store
mover *mover.Mover
buckets map[string][]string // 目标目录 → 文件路径
order []string // 目标目录的创建顺序(保证输出稳定)
}
// New 创建整理器;store 可为 nil(数据库初始化失败时仅不记录信息)
func New(pickerDir, ffprobePath string, st *store.Store) *Organizer {
return &Organizer{
pickerDir: pickerDir,
ffprobePath: ffprobePath,
store: st,
mover: &mover.Mover{},
buckets: make(map[string][]string),
}
}
// add 将文件归入某个子目录
func (o *Organizer) add(f scanner.File, subDir string) {
dir := filepath.Join(o.pickerDir, subDir)
if _, ok := o.buckets[dir]; !ok {
o.order = append(o.order, dir)
}
o.buckets[dir] = append(o.buckets[dir], f.Path)
}
// record 将媒体信息写入数据库(大文件优先用指纹复用已入库的 MD5)
func (o *Organizer) record(f scanner.File, mediaType string, res *probe.ProbeResult) {
st, err := os.Stat(f.Path)
if err != nil {
fmt.Printf("=== 读取文件信息失败: %s, 错误: %v\n", f.Path, err)
return
}
size := st.Size()
md5Str := ""
headMd5, tailMd5 := "", ""
if size > digest.FingerprintThreshold {
if head, tail, ferr := digest.CalcFileFingerprint(f.Path, size); ferr == nil {
headMd5, tailMd5 = head, tail
if existing, ok := o.store.FindMediaByFingerprint(size, head, tail); ok && existing.FileMd5 != "" {
md5Str = existing.FileMd5 // 指纹命中,复用MD5,跳过全量读取
}
}
}
if md5Str == "" {
if md5Str, err = digest.CalcFileMd5(f.Path); err != nil {
fmt.Printf("=== 计算MD5失败: %s, 错误: %v\n", f.Path, err)
return
}
}
codec := res.VideoCodec
if mediaType == "audio" {
codec = res.AudioCodec
}
info := &store.MediaInfo{
FileMd5: md5Str,
FilePath: f.Path,
FileName: f.Name,
FileExt: f.Ext,
FileSize: size,
MimeType: f.Mime,
MediaType: mediaType,
Width: res.Width,
Height: res.Height,
Duration: res.Duration,
Codec: codec,
Format: res.Format,
RawInfo: res.RawJSON,
HeadMd5: headMd5,
TailMd5: tailMd5,
}
if err := o.store.SaveMedia(info); err != nil {
fmt.Printf("=== 保存媒体信息失败: %s, 错误: %v\n", f.Path, err)
}
}
// ProcessImages 处理全部图片:探测尺寸并归类
func (o *Organizer) ProcessImages(images []scanner.File) {
success, fail, ignore := 0, 0, 0
for _, f := range images {
switch {
case strings.EqualFold(f.Ext, ".psd"):
o.add(f, dirImagePSD)
success++
continue
case !probe.IsSupportImage(f.Ext):
o.add(f, dirImageIgnored)
ignore++
continue
}
res, err := probe.ProbeImage(f.Path, o.ffprobePath)
if err != nil {
o.add(f, dirImageErr)
fail++
fmt.Printf("=== 异常图片: %s \n", f.Path)
continue
}
o.record(f, "image", res)
o.add(f, classify.ImageDir(f.Ext, res.Width, res.Height))
success++
}
fmt.Printf("=== 图片处理完毕,成功: %d, 读取异常: %d, 忽略: %d \n\n", success, fail, ignore)
}
// ProcessMedia 处理媒体候选:ffprobe 判定视频/音频/不支持
func (o *Organizer) ProcessMedia(media []scanner.File) {
video, audio, fail, ignore := 0, 0, 0, 0
for _, f := range media {
res, err := probe.ProbeMediaFile(o.ffprobePath, f.Path)
if err != nil {
if strings.HasPrefix(f.Mime, "video/") {
fail++
o.add(f, dirVideoErr)
fmt.Printf("=== 异常视频: %s \n", f.Path)
} else {
ignore++
o.add(f, dirVideoIgnored)
}
continue
}
switch {
case res.HasVideo:
video++
o.record(f, "video", res)
o.add(f, classify.VideoDir(f.Ext, res.Width, res.Height))
case res.HasAudio:
audio++
o.record(f, "audio", res)
o.add(f, dirAudio)
default:
ignore++
o.add(f, dirVideoIgnored)
}
}
fmt.Printf("=== 媒体处理完毕,视频: %d, 音频: %d, 读取异常: %d, 忽略: %d \n\n", video, audio, fail, ignore)
}
// Flush 将所有收集到的文件移动到对应目录,并回填归类去向
func (o *Organizer) Flush() {
for _, dir := range o.order {
moved := o.mover.MoveToDir(o.buckets[dir], dir)
for src, dest := range moved {
if err := o.store.UpdateMediaDest(src, dest); err != nil {
fmt.Printf("=== 更新媒体去向失败: %s, 错误: %v\n", src, err)
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
package probe
import (
"encoding/binary"
"fmt"
"os"
)
// probeBmp 读取 BMP 文件头中的宽高;自顶向下 BMP 的高度为负值,取绝对值
func probeBmp(filePath string) (*ProbeResult, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
var fileHeader struct {
FileType [2]byte
FileSize uint32
Reserved1 uint16
Reserved2 uint16
DataOffset uint32
}
if err := binary.Read(f, binary.LittleEndian, &fileHeader); err != nil {
return nil, err
}
if string(fileHeader.FileType[:]) != "BM" {
return nil, fmt.Errorf("not a valid BMP file")
}
var infoHeader struct {
Size uint32
Width int32
Height int32
Planes uint16
BitsPerPixel uint16
Compression uint32
SizeImage uint32
XPelsPerMeter int32
YPelsPerMeter int32
}
if err := binary.Read(f, binary.LittleEndian, &infoHeader); err != nil {
return nil, err
}
return &ProbeResult{Width: abs(infoHeader.Width), Height: abs(infoHeader.Height)}, nil
}
func abs(v int32) int {
if v < 0 {
return int(-v)
}
return int(v)
}
+32
View File
@@ -0,0 +1,32 @@
package probe
import (
"fmt"
"os"
)
// FFprobeExeName 释放到当前目录的解码器文件名
const FFprobeExeName = "ffprobe.exe"
// FFprobePath 返回当前目录下的 ffprobe 可执行路径(Windows/Unix 均可直接执行)
func FFprobePath() string {
return "." + string(os.PathSeparator) + FFprobeExeName
}
// HasEmbeddedFFprobe 当前平台是否内嵌了 ffprobe
func HasEmbeddedFFprobe() bool {
return len(embeddedFFprobe) > 0
}
// ReleaseFFprobe 释放内嵌的 ffprobe 到当前目录(按当前系统自动选择对应版本)
func ReleaseFFprobe() error {
if len(embeddedFFprobe) == 0 {
return fmt.Errorf("当前系统没有内嵌的ffprobe")
}
return os.WriteFile(FFprobeExeName, embeddedFFprobe, 0777)
}
// RemoveFFprobe 删除当前目录下释放的 ffprobe
func RemoveFFprobe() {
_ = os.Remove(FFprobeExeName)
}
+8
View File
@@ -0,0 +1,8 @@
//go:build darwin
package probe
import _ "embed"
//go:embed bin/ffprobe_mac
var embeddedFFprobe []byte // macOS 版 ffprobe(未入库,见 .gitignore)
@@ -0,0 +1,6 @@
//go:build !darwin && !windows
package probe
// embeddedFFprobe 当前平台没有内嵌的 ffprobe
var embeddedFFprobe []byte
+8
View File
@@ -0,0 +1,8 @@
//go:build windows
package probe
import _ "embed"
//go:embed bin/ffprobe.exe
var embeddedFFprobe []byte // Windows 版 ffprobe(未入库,见 .gitignore)
+76
View File
@@ -0,0 +1,76 @@
package probe
import (
"fmt"
"image"
"os"
"path/filepath"
"strings"
)
// supportImageTypes 支持的标准图片扩展名(大小写不敏感)
var supportImageTypes = []string{
".bmp",
".gif",
".jpg",
".jpeg",
".jpe",
".png",
".webp",
".psb",
".jfif",
}
// IsSupportImage 判断扩展名是否属于支持的图片格式
func IsSupportImage(ext string) bool {
for _, t := range supportImageTypes {
if strings.EqualFold(t, ext) {
return true
}
}
return false
}
// ProbeImage 统一探测图片尺寸:
// 1. 特殊格式(webp/bmp/psd/psb)走专用解码器
// 2. 其余支持格式走 Go 标准库 image.DecodeConfig
// 3. 标准库失败时用 ffprobe 兜底(ffmpeg 支持的图片格式更广,如 tiff/svg 等)
//
// ffprobePath 为空表示不启用 ffprobe 兜底;扩展名不属于支持格式时返回错误。
func ProbeImage(filePath, ffprobePath string) (*ProbeResult, error) {
ext := strings.ToLower(filepath.Ext(filePath))
switch ext {
case ".webp":
return probeWebp(filePath)
case ".bmp":
return probeBmp(filePath)
case ".psd", ".psb":
return probePsd(filePath)
}
if !IsSupportImage(ext) {
return nil, fmt.Errorf("不支持的图片格式: %s", ext)
}
if w, h, err := decodeConfig(filePath); err == nil {
return &ProbeResult{Width: w, Height: h}, nil
}
if ffprobePath != "" {
if res, err := ProbeMediaFile(ffprobePath, filePath); err == nil && res.HasVideo {
return res, nil
}
}
return nil, fmt.Errorf("无法读取图片尺寸: %s", filePath)
}
// decodeConfig 用 Go 标准库读取图片宽高
func decodeConfig(filePath string) (int, int, error) {
f, err := os.Open(filePath)
if err != nil {
return 0, 0, err
}
defer f.Close()
cfg, _, err := image.DecodeConfig(f)
if err != nil {
return 0, 0, err
}
return cfg.Width, cfg.Height, nil
}
+80
View File
@@ -0,0 +1,80 @@
// Package probe 负责媒体信息探测:图片尺寸读取、ffprobe 视频/音频解析与内嵌解码器释放。
package probe
import (
"encoding/json"
"fmt"
"os/exec"
"strconv"
)
// ProbeResult 媒体探测结果(图片/视频/音频通用)
type ProbeResult struct {
HasVideo bool
HasAudio bool
Width int
Height int
Duration float64
VideoCodec string
AudioCodec string
Format string
RawJSON string
}
type streamInfo struct {
Index int `json:"index"`
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Width int `json:"width"`
Height int `json:"height"`
}
type formatInfo struct {
Duration string `json:"duration"`
Format string `json:"format_name"`
}
type probeOutput struct {
Streams []streamInfo `json:"streams"`
Format formatInfo `json:"format"`
}
// ProbeMediaFile 用 ffprobe 探测媒体文件,一次调用同时获取视频/音频流与时长
func ProbeMediaFile(ffprobePath, filePath string) (*ProbeResult, error) {
cmd := exec.Command(ffprobePath, "-v", "error",
"-show_entries", "stream=index,codec_type,codec_name,width,height:format=duration,format_name",
"-of", "json", filePath)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("ffprobe failed: %w", err)
}
var out probeOutput
if err := json.Unmarshal(output, &out); err != nil {
return nil, fmt.Errorf("parse ffprobe output: %w", err)
}
res := &ProbeResult{
Format: out.Format.Format,
RawJSON: string(output),
}
if d, err := strconv.ParseFloat(out.Format.Duration, 64); err == nil {
res.Duration = d
}
for _, s := range out.Streams {
switch s.CodecType {
case "video":
res.HasVideo = true
if res.VideoCodec == "" {
res.VideoCodec = s.CodecName
}
if res.Width == 0 && s.Width > 0 && s.Height > 0 {
res.Width, res.Height = s.Width, s.Height
}
case "audio":
res.HasAudio = true
if res.AudioCodec == "" {
res.AudioCodec = s.CodecName
}
}
}
return res, nil
}
+32
View File
@@ -0,0 +1,32 @@
package probe
import (
"encoding/binary"
"fmt"
"io"
"os"
)
// probePsd 读取 PSD/PSB 格式的宽高(两者共享相同的 8BPS 文件头)
func probePsd(filePath string) (*ProbeResult, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
var header [26]byte
if _, err := io.ReadFull(f, header[:]); err != nil {
return nil, err
}
if string(header[0:4]) != "8BPS" {
return nil, fmt.Errorf("not a valid PSD/PSB file")
}
version := binary.BigEndian.Uint16(header[4:6])
if version != 1 && version != 2 {
return nil, fmt.Errorf("unsupported PSD/PSB version: %d", version)
}
height := int(binary.BigEndian.Uint32(header[14:18]))
width := int(binary.BigEndian.Uint32(header[18:22]))
return &ProbeResult{Width: width, Height: height}, nil
}
+22
View File
@@ -0,0 +1,22 @@
package probe
import (
"fmt"
"os"
"golang.org/x/image/webp"
)
// probeWebp 读取 WebP 图片宽高(只解码配置,不加载完整像素数据)
func probeWebp(filePath string) (*ProbeResult, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
cfg, err := webp.DecodeConfig(f)
if err != nil {
return nil, fmt.Errorf("decode webp: %w", err)
}
return &ProbeResult{Width: cfg.Width, Height: cfg.Height}, nil
}
+138
View File
@@ -0,0 +1,138 @@
// Package scanner 负责递归扫描文件、读取 MIME 并按类型过滤。
package scanner
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"kenaito-media-picker/internal/probe"
"github.com/gabriel-vasile/mimetype"
"github.com/redmask-hb/GoSimplePrint/goPrint"
)
// File 扫描到的文件信息
type File struct {
Path string // 完整路径
Name string // 文件名(含扩展名)
Ext string // 扩展名(含点)
Mime string // MIME 类型
}
// Result 过滤结果
type Result struct {
Files []File // 全部文件
Images []File // 图片文件
Media []File // 待 ffprobe 确认的媒体候选
}
// Options 扫描选项
type Options struct {
RootDir string // 扫描根目录
SkipDir string // 白名单输出目录,整目录跳过
SkipNames []string // 按文件名跳过(数据库、ffprobe 等)
}
// Scan 递归扫描目录并读取每个文件的 MIME 信息
func Scan(opts Options) (*Result, error) {
res := &Result{}
err := filepath.Walk(opts.RootDir, func(currentPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if opts.SkipDir != "" && currentPath == opts.SkipDir {
return filepath.SkipDir
}
return nil
}
for _, n := range opts.SkipNames {
if info.Name() == n {
return nil
}
}
res.Files = append(res.Files, File{
Path: currentPath,
Name: info.Name(),
Ext: path.Ext(currentPath),
})
return nil
})
if err != nil {
return nil, err
}
total := len(res.Files)
fmt.Printf("=== 文件总数: %d \n", total)
bar := goPrint.NewBar(100)
bar.SetNotice("=== 扫描文件Mime:")
bar.SetGraph(">")
bar.SetNoticeColor(goPrint.FontColor.Red)
for i := range res.Files {
res.Files[i].Mime = detectMime(res.Files[i].Path)
bar.PrintBar(percent(i+1, total))
}
bar.PrintEnd("=== Finish")
return res, nil
}
// Filter 将文件分为图片与媒体候选
func (r *Result) Filter() {
for _, f := range r.Files {
if strings.Contains(f.Mime, "image/") || probe.IsSupportImage(f.Ext) || strings.EqualFold(f.Ext, ".psd") {
r.Images = append(r.Images, f)
continue
}
if isPotentialMedia(f.Mime) {
r.Media = append(r.Media, f)
continue
}
// 明显的非媒体文件(代码/文档/压缩包等)直接不处理
}
}
// detectMime 读取文件的 MIME 类型
func detectMime(filePath string) string {
mt, err := mimetype.DetectFile(filePath)
if err != nil {
return ""
}
return mt.String()
}
// isPotentialMedia 判断是否可能是媒体文件(交由 ffprobe 进一步确认)
func isPotentialMedia(fileMime string) bool {
if fileMime == "" || fileMime == "application/octet-stream" {
return true
}
if strings.HasPrefix(fileMime, "text/") {
return false
}
if strings.HasPrefix(fileMime, "video/") || strings.HasPrefix(fileMime, "audio/") || strings.HasPrefix(fileMime, "image/") {
return true
}
switch fileMime {
case "application/zip", "application/gzip", "application/x-gzip", "application/pdf",
"application/json", "application/xml", "application/x-httpd-php", "application/x-sh",
"application/javascript", "application/x-7z-compressed", "application/x-rar-compressed",
"application/x-tar", "application/x-bzip2", "application/x-archive", "application/vnd.sqlite3",
"application/vnd.microsoft.portable-executable", "application/x-dosexec", "application/x-executable",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint":
return false
}
return true
}
// percent 计算百分比(0~100),total 为 0 时返回 100
func percent(part, total int) int {
if total <= 0 {
return 100
}
return int(float64(part) / float64(total) * 100)
}
+30 -23
View File
@@ -1,3 +1,4 @@
// Package store 提供媒体信息的 SQLite 持久化(xorm + 纯 Go 驱动,免 CGO)。
package store package store
import ( import (
@@ -41,56 +42,62 @@ type MediaInfo struct {
UpdatedAt time.Time `xorm:"updated 'updated_at'"` // 最近更新时间 UpdatedAt time.Time `xorm:"updated 'updated_at'"` // 最近更新时间
} }
var engine *xorm.Engine // Store 封装 SQLite 存取,按实例持有连接,不再依赖全局状态
type Store struct {
engine *xorm.Engine
}
// Init 初始化数据库引擎并同步表结构 // Open 打开(或创建)数据库并同步表结构
func Init(dbPath string) error { func Open(dbPath string) (*Store, error) {
e, err := xorm.NewEngine("sqlite3", dbPath) e, err := xorm.NewEngine("sqlite3", dbPath)
if err != nil { if err != nil {
return err return nil, err
} }
e.ShowSQL(false) e.ShowSQL(false)
e.SetMaxOpenConns(1) // sqlite同一时刻只允许一个写连接 e.SetMaxOpenConns(1) // sqlite同一时刻只允许一个写连接
engine = e if err := e.Sync(new(MediaInfo)); err != nil {
return engine.Sync(new(MediaInfo)) _ = e.Close()
return nil, err
}
return &Store{engine: e}, nil
} }
// Close 关闭数据库连接 // Close 关闭数据库连接
func Close() error { func (s *Store) Close() error {
if engine != nil { if s == nil || s.engine == nil {
err := engine.Close() return nil
engine = nil
return err
} }
return nil err := s.engine.Close()
s.engine = nil
return err
} }
// SaveMedia 按MD5去重保存/更新媒体信息 // SaveMedia 按MD5去重保存/更新媒体信息
func SaveMedia(info *MediaInfo) error { func (s *Store) SaveMedia(info *MediaInfo) error {
if engine == nil { if s == nil || s.engine == nil {
return nil return nil
} }
existing := new(MediaInfo) existing := new(MediaInfo)
has, err := engine.Where("file_md5 = ?", info.FileMd5).Get(existing) has, err := s.engine.Where("file_md5 = ?", info.FileMd5).Get(existing)
if err != nil { if err != nil {
return err return err
} }
if has { if has {
info.Id = existing.Id info.Id = existing.Id
_, err = engine.ID(existing.Id).Update(info) _, err = s.engine.ID(existing.Id).Update(info)
return err return err
} }
_, err = engine.Insert(info) _, err = s.engine.Insert(info)
return err return err
} }
// FindMediaByFingerprint 按 (文件大小 + 头256KB + 尾256KB) 查找已入库记录,避免对大文件重复计算全量MD5 // FindMediaByFingerprint 按 (文件大小 + 头256KB + 尾256KB) 查找已入库记录,避免对大文件重复计算全量MD5
func FindMediaByFingerprint(size int64, headMd5 string, tailMd5 string) (*MediaInfo, bool) { func (s *Store) FindMediaByFingerprint(size int64, headMd5, tailMd5 string) (*MediaInfo, bool) {
if engine == nil || headMd5 == "" || tailMd5 == "" { if s == nil || s.engine == nil || headMd5 == "" || tailMd5 == "" {
return nil, false return nil, false
} }
info := new(MediaInfo) info := new(MediaInfo)
has, err := engine.Where("file_size = ? AND head_md5 = ? AND tail_md5 = ?", size, headMd5, tailMd5).Get(info) has, err := s.engine.Where("file_size = ? AND head_md5 = ? AND tail_md5 = ?", size, headMd5, tailMd5).Get(info)
if err != nil || !has { if err != nil || !has {
return nil, false return nil, false
} }
@@ -98,11 +105,11 @@ func FindMediaByFingerprint(size int64, headMd5 string, tailMd5 string) (*MediaI
} }
// UpdateMediaDest 按源路径回填文件的归类目录与去向 // UpdateMediaDest 按源路径回填文件的归类目录与去向
func UpdateMediaDest(filePath string, destDir string) error { func (s *Store) UpdateMediaDest(filePath, destDir string) error {
if engine == nil { if s == nil || s.engine == nil {
return nil return nil
} }
_, err := engine.Where("file_path = ?", filePath).Update(&MediaInfo{ _, err := s.engine.Where("file_path = ?", filePath).Update(&MediaInfo{
Category: filepath.Base(destDir), Category: filepath.Base(destDir),
DestDir: destDir, DestDir: destDir,
}) })
+43 -31
View File
@@ -1,58 +1,70 @@
package main package main
import ( import (
_ "embed"
"fmt" "fmt"
_ "image/gif" // 导入gif支持
_ "image/jpeg" // 导入jpeg支持
_ "image/png" // 导入png支持
"kenaito-media-picker/core"
"kenaito-media-picker/store"
"kenaito-media-picker/util"
"kenaito-media-picker/vars"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"kenaito-media-picker/internal/organize"
"kenaito-media-picker/internal/probe"
"kenaito-media-picker/internal/scanner"
"kenaito-media-picker/internal/store"
) )
func main() { func main() {
fmt.Println("=== 警告:该程序会改变目录结构,将在5秒后开始执行") fmt.Println("=== 警告:该程序会改变目录结构,将在5秒后开始执行")
time.Sleep(time.Second * 5) time.Sleep(time.Second * 5)
rootDir, err := os.Getwd() rootDir, err := os.Getwd()
if err != nil { if err != nil {
fmt.Println("=== 获取当前路径异常", err) fmt.Println("=== 获取当前路径异常", err)
return return
} }
pickerDir := filepath.Join(rootDir, filepath.Base(rootDir)+"-picker") pickerDir := filepath.Join(rootDir, filepath.Base(rootDir)+"-picker")
vars.GlobalPickerDir = pickerDir
fmt.Printf("=== 整理输出目录(白名单): %s \n", pickerDir) fmt.Printf("=== 整理输出目录(白名单): %s \n", pickerDir)
scanner := core.FileScanner{}
scanner.DoScan(rootDir) // 1. 扫描并过滤(跳过输出目录、数据库与 ffprobe 相关文件)
scanner.DoFilter() res, err := scanner.Scan(scanner.Options{
// 初始化媒体信息数据库(失败不影响整理,仅无法记录) RootDir: rootDir,
if err := store.Init(filepath.Join(rootDir, store.DBFileName)); err != nil { SkipDir: pickerDir,
SkipNames: []string{store.DBFileName, probe.FFprobeExeName, "ffprobe_mac"},
})
if err != nil {
fmt.Println("=== 扫描失败:", err)
return
}
res.Filter()
// 2. 初始化媒体信息数据库(失败不影响整理,仅无法记录)
st, err := store.Open(filepath.Join(rootDir, store.DBFileName))
if err != nil {
fmt.Println("=== 初始化数据库失败(继续整理, 不记录媒体信息):", err) fmt.Println("=== 初始化数据库失败(继续整理, 不记录媒体信息):", err)
st = nil
} }
defer store.Close() if st != nil {
// 释放内嵌ffprobe(按当前系统);失败只跳过媒体整理,图片照常处理 defer st.Close()
ffprobeReady := true }
if len(vars.GlobalImagePathList) > 0 || len(vars.GlobalMediaPathList) > 0 {
if err := util.ReleaseFFprobe(); err != nil { // 3. 释放内嵌 ffprobe(按当前系统);失败只跳过媒体整理,图片照常处理
ffprobePath := ""
if (len(res.Images) > 0 || len(res.Media) > 0) && probe.HasEmbeddedFFprobe() {
if err := probe.ReleaseFFprobe(); err != nil {
fmt.Println("=== 释放解码器失败, 视频/音频整理将跳过(图片照常处理):", err) fmt.Println("=== 释放解码器失败, 视频/音频整理将跳过(图片照常处理):", err)
ffprobeReady = false } else {
ffprobePath = probe.FFprobePath()
defer probe.RemoveFFprobe()
} }
} }
defer util.RemoveFFprobe()
if len(vars.GlobalImagePathList) > 0 { // 4. 整理
core.DoHandleImage(pickerDir) org := organize.New(pickerDir, ffprobePath, st)
if len(res.Images) > 0 {
org.ProcessImages(res.Images)
} }
if len(vars.GlobalMediaPathList) > 0 && ffprobeReady { if len(res.Media) > 0 && ffprobePath != "" {
core.DoHandleMedia(pickerDir) org.ProcessMedia(res.Media)
}
// 所有移动完成后,回填文件的归类目录与去向
for src, dest := range vars.GlobalPath2DestDirMap {
if err := store.UpdateMediaDest(src, dest); err != nil {
fmt.Printf("=== 更新媒体去向失败: %s, 错误: %v\n", src, err)
}
} }
org.Flush()
fmt.Println("=== 整理完成")
} }
-19
View File
@@ -1,19 +0,0 @@
package util
import (
"strconv"
)
// String2int 字符串转int
func String2int(str string) int {
intValue, err := strconv.Atoi(str)
if err != nil {
return 0
}
return intValue
}
// CalcPercentage 计算percentage相对于total的百分比
func CalcPercentage(percentage int, total int) int {
return int(float64(percentage) / float64(total) * 100)
}
-162
View File
@@ -1,162 +0,0 @@
package util
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gabriel-vasile/mimetype"
"io"
"io/ioutil"
"os"
"path/filepath"
)
// CreateDir 创建目录(含递归创建父目录)
func CreateDir(dirPath string) bool {
err := os.MkdirAll(dirPath, 0755)
if err != nil {
fmt.Printf("=== 创建目录失败: %s, 错误: %v\n", dirPath, err)
}
return err == nil
}
// GetFileDirectory 获取文件所在文件夹
func GetFileDirectory(filePath string) string {
directoryPath, _ := filepath.Split(filePath)
return directoryPath
}
// ReadFileMimeInfo 获取文件mime信息,失败时返回nil
func ReadFileMimeInfo(filepath string) *mimetype.MIME {
mt, err := mimetype.DetectFile(filepath)
if err != nil {
fmt.Printf("=== 读取Mime信息失败: %s, 错误: %v\n", filepath, err)
return nil
}
return mt
}
// CheckFileIsExist 判断文件是否存在,存在返回 true,不存在返回false
func CheckFileIsExist(filename string) bool {
var exist = true
if _, err := os.Stat(filename); os.IsNotExist(err) {
exist = false
}
return exist
}
// WriteByteArraysToFile 写字节数组到文件
func WriteByteArraysToFile(content []byte, filename string) error {
return ioutil.WriteFile(filename, content, 0777)
}
// CalcFileMd5 计算文件MD5(分块读取,适合大文件)
func CalcFileMd5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
h := md5.New()
if _, err := io.Copy(h, file); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// MD5指纹加速相关常量
const (
FingerprintThreshold = 1 << 20 // 文件大于1MB才使用指纹复用,避免全量MD5
fingerprintHeadSize = 256 << 10 // 文件头采样256KB
fingerprintTailSize = 256 << 10 // 文件尾采样256KB
)
// CalcFileFingerprint 计算文件的头部与尾部采样MD5(只读取很少字节,用于快速识别相同文件)
func CalcFileFingerprint(filePath string, size int64) (headMd5 string, tailMd5 string, err error) {
file, err := os.Open(filePath)
if err != nil {
return "", "", err
}
defer file.Close()
h := md5.New()
if _, err := io.CopyN(h, file, fingerprintHeadSize); err != nil {
return "", "", err
}
headMd5 = hex.EncodeToString(h.Sum(nil))
h = md5.New()
if _, err := file.Seek(size-fingerprintTailSize, io.SeekStart); err != nil {
return "", "", err
}
if _, err := io.CopyN(h, file, fingerprintTailSize); err != nil {
return "", "", err
}
tailMd5 = hex.EncodeToString(h.Sum(nil))
return headMd5, tailMd5, nil
}
//// 移动文件到根目录
//func renameFile(rootDir string, modelType string, videoList []string, pathSeparator string) {
// total := len(videoList)
// var count = 0
// bar := goPrint.NewBar(100)
// bar.SetNotice("=== 重命名文件:")
// bar.SetGraph(">")
// for _, videoFilePath := range videoList {
// wh := videoPath2WidthHeightTagMap[videoFilePath]
// fileName := vars.GlobalFilePath2FileNameMap[videoFilePath]
// if strings.Contains(fileName, videoTag) { // 处理过了
// fileNames := strings.Split(fileName, videoTag)
// if len(fileNames) == 2 {
// fileName = fileNames[1]
// targetFilePath := rootDir + pathSeparator + "[" + videoPath2DurationMap[videoFilePath] + "]" + modelType + wh + videoTag + fileName
// err := os.Rename(videoFilePath, targetFilePath)
// if err != nil {
// fmt.Printf("=== 重命名异常: %s \n", videoFilePath)
// }
// }
// } else {
// targetFilePath := rootDir + pathSeparator + "[" + videoPath2DurationMap[videoFilePath] + "]" + modelType + wh + videoTag + " - " + fileName
// err := os.Rename(videoFilePath, targetFilePath)
// if err != nil {
// fmt.Printf("=== 重命名异常: %s \n", videoFilePath)
// }
// }
// count = count + 1
// bar.PrintBar(util.CalcPercentage(count, total))
// }
// bar.PrintEnd("=== Finish")
//}
//
//// 移动文件到原目录
//func renameFileV2(modelType string, videoList []string) {
// total := len(videoList)
// var count = 0
// bar := goPrint.NewBar(100)
// bar.SetNotice("=== 重命名文件:")
// bar.SetGraph(">")
// for _, videoFilePath := range videoList {
// wh := videoPath2WidthHeightTagMap[videoFilePath]
// fileName := vars.GlobalFilePath2FileNameMap[videoFilePath]
// filePath := util.GetFileDirectory(videoFilePath)
// if strings.Contains(fileName, videoTag) { // 处理过了
// fileNames := strings.Split(fileName, videoTag)
// if len(fileNames) == 2 {
// fileName = fileNames[1]
// targetFilePath := filePath + "[" + videoPath2DurationMap[videoFilePath] + "]" + modelType + wh + videoTag + fileName
// err := os.Rename(videoFilePath, targetFilePath)
// if err != nil {
// fmt.Printf("=== 重命名异常: %s \n", videoFilePath)
// }
// }
// } else {
// targetFilePath := filePath + "[" + videoPath2DurationMap[videoFilePath] + "]" + modelType + wh + videoTag + " - " + fileName
// err := os.Rename(videoFilePath, targetFilePath)
// if err != nil {
// fmt.Printf("=== 重命名异常: %s \n", videoFilePath)
// }
// }
// count = count + 1
// bar.PrintBar(util.CalcPercentage(count, total))
// }
// bar.PrintEnd("=== Finish")
//}
-43
View File
@@ -1,43 +0,0 @@
package util
import (
"image"
"os"
"strings"
)
var supportImageTypes = []string{
".bmp",
".gif",
".jpg",
".jpeg",
".jpe",
".png",
".webp",
".psb",
".jfif",
}
// IsSupportImage 判断是否属于支持的图片文件
func IsSupportImage(imageType string) bool {
for _, supportImageType := range supportImageTypes {
if strings.EqualFold(supportImageType, strings.ToLower(imageType)) {
return true
}
}
return false
}
// ReadImageInfo 读取一般图片文件信息
func ReadImageInfo(filePath string) (err error, width int, height int) {
file, err := os.Open(filePath) // 图片文件路径
if err != nil {
return err, 0, 0
}
defer file.Close()
img, _, err := image.DecodeConfig(file)
if err != nil {
return err, 0, 0
}
return nil, img.Width, img.Height
}
-221
View File
@@ -1,221 +0,0 @@
package util
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"kenaito-media-picker/vars"
"github.com/redmask-hb/GoSimplePrint/goPrint"
uuid "github.com/satori/go.uuid"
)
// FFprobeExeName 内嵌解码器文件名
const FFprobeExeName = "ffprobe.exe"
// FFprobeExePath 返回带当前目录前缀的可执行路径(Windows/Unix均可直接执行)
func FFprobeExePath() string {
return "." + string(os.PathSeparator) + FFprobeExeName
}
// ReleaseFFprobe 释放内嵌的ffprobe到当前目录(按当前系统自动选择对应版本)
func ReleaseFFprobe() error {
if len(vars.VideoDecodeTool) == 0 {
return fmt.Errorf("当前系统没有内嵌的ffprobe")
}
return WriteByteArraysToFile(vars.VideoDecodeTool, FFprobeExeName)
}
// RemoveFFprobe 删除当前目录下的ffprobe
func RemoveFFprobe() {
if CheckFileIsExist(FFprobeExeName) {
_ = os.Remove(FFprobeExeName)
}
}
// StreamInfo ffprobe流信息
type StreamInfo struct {
Index int `json:"index"`
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
Width int `json:"width"`
Height int `json:"height"`
}
// FormatInfo ffprobe容器信息
type FormatInfo struct {
Duration string `json:"duration"`
Format string `json:"format_name"`
}
// ProbeResult ffprobe探测结果
type ProbeResult struct {
Streams []StreamInfo `json:"streams"`
Format FormatInfo `json:"format"`
raw string
}
// VideoResolution 返回第一个有效的视频分辨率
func (p *ProbeResult) VideoResolution() (int, int) {
for _, s := range p.Streams {
if s.CodecType == "video" && s.Width > 0 && s.Height > 0 {
return s.Width, s.Height
}
}
return 0, 0
}
// HasAudio 是否包含音频流
func (p *ProbeResult) HasAudio() bool {
for _, s := range p.Streams {
if s.CodecType == "audio" {
return true
}
}
return false
}
// VideoCodec 第一个视频流的编码名称
func (p *ProbeResult) VideoCodec() string {
for _, s := range p.Streams {
if s.CodecType == "video" {
return s.CodecName
}
}
return ""
}
// AudioCodec 第一个音频流的编码名称
func (p *ProbeResult) AudioCodec() string {
for _, s := range p.Streams {
if s.CodecType == "audio" {
return s.CodecName
}
}
return ""
}
// FormatName 容器格式名称
func (p *ProbeResult) FormatName() string {
return p.Format.Format
}
// RawJSON 原始ffprobe JSON输出
func (p *ProbeResult) RawJSON() string {
return p.raw
}
// DurationSeconds 容器时长(秒),无法解析时返回0
func (p *ProbeResult) DurationSeconds() float64 {
d, err := strconv.ParseFloat(p.Format.Duration, 64)
if err != nil {
return 0
}
return d
}
// ProbeMediaFile 用ffprobe探测媒体文件,一次调用同时获取视频/音频流与时长
func ProbeMediaFile(ffprobeExecPath string, filePath string) (*ProbeResult, error) {
cmd := exec.Command(ffprobeExecPath, "-v", "error",
"-show_entries", "stream=index,codec_type,codec_name,width,height:format=duration,format_name",
"-of", "json", filePath)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("ffprobe failed: %w", err)
}
var result ProbeResult
if err := json.Unmarshal(output, &result); err != nil {
return nil, fmt.Errorf("failed to parse ffprobe output: %w", err)
}
result.raw = string(output)
return &result, nil
}
// IsPotentialMedia 判断是否可能是媒体文件(交由ffprobe进一步确认)
func IsPotentialMedia(fileMime string) bool {
if fileMime == "" || fileMime == "application/octet-stream" {
return true
}
if strings.HasPrefix(fileMime, "text/") {
return false
}
if strings.HasPrefix(fileMime, "video/") || strings.HasPrefix(fileMime, "audio/") || strings.HasPrefix(fileMime, "image/") {
return true
}
switch fileMime {
case "application/zip", "application/gzip", "application/x-gzip", "application/pdf",
"application/json", "application/xml", "application/x-httpd-php", "application/x-sh",
"application/javascript", "application/x-7z-compressed", "application/x-rar-compressed",
"application/x-tar", "application/x-bzip2", "application/x-archive", "application/vnd.sqlite3",
"application/vnd.microsoft.portable-executable", "application/x-dosexec", "application/x-executable",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint":
return false
}
return true
}
// DoMoveFileToDir 批量移动文件到目录
func DoMoveFileToDir(filePathList []string, videoDirPath string) {
total := len(filePathList)
var count = 0
bar := goPrint.NewBar(100)
bar.SetNotice("=== 移动文件到目录:")
bar.SetGraph(">")
pathSeparator := string(os.PathSeparator)
for _, videoFilePath := range filePathList {
moveFileToDir(videoFilePath, videoDirPath+pathSeparator)
vars.GlobalPath2DestDirMap[videoFilePath] = videoDirPath // 记录去向
count = count + 1
bar.PrintBar(CalcPercentage(count, total))
}
bar.PrintEnd("=== Finish")
}
// 移动文件到目录,跨文件系统时自动 fallback 为 copy+delete,目标文件名使用 uuid+原文件名防止同名覆盖
func moveFileToDir(sourceFilePath string, targetDirectory string) bool {
fileName := strings.ReplaceAll(uuid.NewV4().String(), "-", "") + "_" + filepath.Base(sourceFilePath)
targetFilePath := targetDirectory + fileName
err := os.Rename(sourceFilePath, targetFilePath)
if err == nil {
return true
}
// 跨文件系统等场景:fallback copy + delete
if err := copyFile(sourceFilePath, targetFilePath); err != nil {
fmt.Printf("=== 复制文件失败, 源: %s, 目标: %s, 错误: %v\n", sourceFilePath, targetFilePath, err)
return false
}
if err := os.Remove(sourceFilePath); err != nil {
fmt.Printf("=== 删除源文件失败: %s, 错误: %v\n", sourceFilePath, err)
// 复制成功但删除失败,文件已存在于两处,返回 true 表示目标已就位
}
return true
}
// copyFile 逐块复制文件
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return out.Close()
}
-8
View File
@@ -1,8 +0,0 @@
//go:build darwin
package vars
import _ "embed"
//go:embed bin/ffprobe_mac
var VideoDecodeTool []byte // 需在 vars/bin/ 下放置 macOS 版 ffprobe(未入库,见 .gitignore)
-6
View File
@@ -1,6 +0,0 @@
//go:build !darwin && !windows
package vars
// VideoDecodeTool 当前平台没有内嵌的ffprobe
var VideoDecodeTool []byte
-8
View File
@@ -1,8 +0,0 @@
//go:build windows
package vars
import _ "embed"
//go:embed bin/ffprobe.exe
var VideoDecodeTool []byte // 需在 vars/bin/ 下放置 Windows 版 ffprobe(未入库,见 .gitignore)
-11
View File
@@ -1,11 +0,0 @@
package vars
var GlobalFilePathList []string // 所有文件的路径
var GlobalFilePath2MimeInfoMap = make(map[string]string) // 文件路径和mime信息的映射关系
var GlobalFilePath2FileNameMap = make(map[string]string) // 文件路径和文件名称的映射关系
var GlobalFilePath2FileExtMap = make(map[string]string) // 文件路径和文件扩展名的映射关系
var GlobalImagePathList []string // 所有图片文件的路径
var GlobalMediaPathList []string // 待ffprobe确认的媒体候选文件
var GlobalAudioPathList []string // 已确认的音频文件路径
var GlobalPickerDir string // 已整理文件的输出目录(白名单,扫描时跳过)
var GlobalPath2DestDirMap = make(map[string]string) // 文件路径 → 移动到的目录(去向)
-81
View File
@@ -1,81 +0,0 @@
package vars
var ImageIgnorePathList []string // 忽略的文件路径
var ImageReadErrorPathList []string // 读取信息异常的路径
var ImagePath2WidthHeightMap = make(map[string]string) // 图片路径和宽高比
var ImageGifList []string
var ImagePsdList []string
var ImageHorizontalNormalList []string
var ImageHorizontal1KList []string
var ImageHorizontal2KList []string
var ImageHorizontal3KList []string
var ImageHorizontal4KList []string
var ImageHorizontal5KList []string
var ImageHorizontal6KList []string
var ImageHorizontal7KList []string
var ImageHorizontal8KList []string
var ImageHorizontal9KList []string
var ImageHorizontal10KList []string
var ImageHorizontal11KList []string
var ImageHorizontal12KList []string
var ImageHorizontal13KList []string
var ImageHorizontal14KList []string
var ImageHorizontal15KList []string
var ImageHorizontal16KList []string
var ImageHorizontal17KList []string
var ImageHorizontal18KList []string
var ImageHorizontal19KList []string
var ImageHorizontalHKList []string
var ImageHorizontalStandard720PList []string
var ImageHorizontalStandard1D5KList []string
var ImageHorizontalStandard1080PList []string
var ImageHorizontalStandard2DKList []string
var ImageHorizontalStandard2D5KList []string
var ImageHorizontalStandardUltraWide2List []string
var ImageHorizontalStandardUltraWide3List []string
var ImageHorizontalStandardUltraWide4List []string
var ImageHorizontalStandard4KList []string
var ImageHorizontalStandard4KHList []string
var ImageHorizontalStandard8KList []string
var ImageVerticalNormalList []string
var ImageVertical1KList []string
var ImageVertical2KList []string
var ImageVertical3KList []string
var ImageVertical4KList []string
var ImageVertical5KList []string
var ImageVertical6KList []string
var ImageVertical7KList []string
var ImageVertical8KList []string
var ImageVertical9KList []string
var ImageVertical10KList []string
var ImageVertical11KList []string
var ImageVertical12KList []string
var ImageVertical13KList []string
var ImageVertical14KList []string
var ImageVertical15KList []string
var ImageVertical16KList []string
var ImageVertical17KList []string
var ImageVertical18KList []string
var ImageVertical19KList []string
var ImageVerticalHKList []string
var ImageSquareNormalList []string
var ImageSquare1KList []string
var ImageSquare2KList []string
var ImageSquare3KList []string
var ImageSquare4KList []string
var ImageSquare5KList []string
var ImageSquare6KList []string
var ImageSquare7KList []string
var ImageSquare8KList []string
var ImageSquare9KList []string
var ImageSquare10KList []string
var ImageSquare11KList []string
var ImageSquare12KList []string
var ImageSquare13KList []string
var ImageSquare14KList []string
var ImageSquare15KList []string
var ImageSquare16KList []string
var ImageSquare17KList []string
var ImageSquare18KList []string
var ImageSquare19KList []string
var ImageSquareHKList []string
-82
View File
@@ -1,82 +0,0 @@
package vars
var VideoIgnorePathList []string // 忽略的文件路径
var VideoReadErrorPathList []string // 读取信息异常的路径
var VideoPath2WidthHeightMap = make(map[string]string) // 视频路径和宽高比
var VideoHorizontalNormalList []string
var VideoHorizontalGifList []string
var VideoHorizontal1KList []string
var VideoHorizontal2KList []string
var VideoHorizontal3KList []string
var VideoHorizontal4KList []string
var VideoHorizontal5KList []string
var VideoHorizontal6KList []string
var VideoHorizontal7KList []string
var VideoHorizontal8KList []string
var VideoHorizontal9KList []string
var VideoHorizontal10KList []string
var VideoHorizontal11KList []string
var VideoHorizontal12KList []string
var VideoHorizontal13KList []string
var VideoHorizontal14KList []string
var VideoHorizontal15KList []string
var VideoHorizontal16KList []string
var VideoHorizontal17KList []string
var VideoHorizontal18KList []string
var VideoHorizontal19KList []string
var VideoHorizontalHKList []string
var VideoHorizontalStandard720PList []string
var VideoHorizontalStandard1D5PList []string
var VideoHorizontalStandard1080PList []string
var VideoHorizontalStandard2DKList []string
var VideoHorizontalStandard2D5KList []string
var VideoHorizontalStandardUltraWide2List []string
var VideoHorizontalStandardUltraWide3List []string
var VideoHorizontalStandardUltraWide4List []string
var VideoHorizontalStandard4KList []string
var VideoHorizontalStandard4KHList []string
var VideoHorizontalStandard8KList []string
var VideoVerticalNormalList []string
var VideoVerticalGifList []string
var VideoVertical1KList []string
var VideoVertical2KList []string
var VideoVertical3KList []string
var VideoVertical4KList []string
var VideoVertical5KList []string
var VideoVertical6KList []string
var VideoVertical7KList []string
var VideoVertical8KList []string
var VideoVertical9KList []string
var VideoVertical10KList []string
var VideoVertical11KList []string
var VideoVertical12KList []string
var VideoVertical13KList []string
var VideoVertical14KList []string
var VideoVertical15KList []string
var VideoVertical16KList []string
var VideoVertical17KList []string
var VideoVertical18KList []string
var VideoVertical19KList []string
var VideoVerticalHKList []string
var VideoSquareNormalList []string
var VideoSquareGifList []string
var VideoSquare1KList []string
var VideoSquare2KList []string
var VideoSquare3KList []string
var VideoSquare4KList []string
var VideoSquare5KList []string
var VideoSquare6KList []string
var VideoSquare7KList []string
var VideoSquare8KList []string
var VideoSquare9KList []string
var VideoSquare10KList []string
var VideoSquare11KList []string
var VideoSquare12KList []string
var VideoSquare13KList []string
var VideoSquare14KList []string
var VideoSquare15KList []string
var VideoSquare16KList []string
var VideoSquare17KList []string
var VideoSquare18KList []string
var VideoSquare19KList []string
var VideoSquareHKList []string